Initial commit

This commit is contained in:
2020-10-07 10:37:15 +02:00
commit ce5f440392
28157 changed files with 4429172 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@@ -0,0 +1,67 @@
GitHub contributors:
--------------------------------
- 123monsite-regis
- Alex Even
- Alex Sampaio
- André
- Bastien Bieri
- Clotaire 202 ecommerce
- Damien Metzger
- David Gonzalez
- Edvinas Gurevicius
- Francois Gaillard
- François-Marie de Jouvencel
- GoT
- Gregory Roussac
- Gytis Škėma
- Hashem
- Hendrik Luup
- Jerome Nadaud
- Jonathan Lelievre
- Julien Bourdeau
- Julius Zukauskas
- Jérôme Nadaud
- Krystian Podemski
- MathiasReker
- Mathieu Ferment
- Matthieu Rolland
- Maxime Biloé
- Michel ANTOINE
- Mickaël Andrieu
- Nico
- Pablo Borowicz
- Pavel Novitsky
- PeNov
- Pierre RAMBAUD
- PrestaSafe
- Progi1984
- Quetzacoalt91
- Robert Keresnyei
- Rokas Zygmantas
- Rémi Gaillard
- Sacha Froment
- Samir Shah
- Stomp9
- Thibaud Chauviere
- Thierry Marianne
- Thomas
- Thomas Nabord
- Veebipoed.ee
- Xavier
- Zebx
- alex4102
- djfm
- fojt-cz
- gRoussac
- indesign47
- iqit-commerce
- iqit-commerce (Marcin Sz)
- joce
- jocelyn fournier
- jolelievre
- kermes
- marionf
- matks
- matrix
- raph
- sadlyblue

View File

@@ -0,0 +1,47 @@
Academic Free License ("AFL") v. 3.0
This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
a) to reproduce the Original Work in copies, either alone or as part of a collective work;
b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
c) to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.

View File

@@ -0,0 +1,56 @@
# Faceted search module
[![Build Status](https://travis-ci.com/PrestaShop/ps_facetedsearch.svg?branch=master)](https://travis-ci.com/PrestaShop/ps_facetedsearch)
[![Latest Stable Version](https://poser.pugx.org/PrestaShop/ps_facetedsearch/v)](//packagist.org/packages/PrestaShop/ps_facetedsearch)
[![Total Downloads](https://poser.pugx.org/PrestaShop/ps_facetedsearch/downloads)](//packagist.org/packages/PrestaShop/ps_facetedsearch)
[![GitHub license](https://img.shields.io/github/license/PrestaShop/ps_facetedsearch)](https://github.com/PrestaShop/ps_facetedsearch/LICENSE.md)
## About
Displays a block with layered navigation filters.
## Multistore compatibility
This module is partially compatible with the multistore feature. Some of its options might not be available.
## Reporting issues
You can report issues with this module in the main PrestaShop repository. [Click here to report an issue][report-issue].
## Requirements
Required only for development:
- npm
- composer
## Installation
Install all dependencies. Be careful, you need NodeJs 10+.
```
npm install
composer install
```
## Usage
```
npm run dev # Watch js/css files for changes
npm run build # Build for production
```
## Contributing
PrestaShop modules are open source extensions to the [PrestaShop e-commerce platform][prestashop]. Everyone is welcome and even encouraged to contribute with their own improvements!
Just make sure to follow our [contribution guidelines][contribution-guidelines].
## License
This module is released under the [Academic Free License 3.0][AFL-3.0]
[report-issue]: https://github.com/PrestaShop/PrestaShop/issues/new/choose
[prestashop]: https://www.prestashop.com/
[contribution-guidelines]: https://devdocs.prestashop.com/1.7/contribute/contribution-guidelines/project-modules/
[AFL-3.0]: https://opensource.org/licenses/AFL-3.0

View File

@@ -0,0 +1,50 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.bootstrap {
.filter_list .filter_list_item {
display: table;
width: 100%;
padding: 5px 0;
margin-bottom: 4px;
background-color: white;
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
}
.filter_panel {
min-height: 20px;
padding: 7px 7px 0px 7px;
margin-bottom: 20px;
background-color: #ebebeb;
border: 1px solid #d9d9d9;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
header {
margin-bottom: 7px;
}
}
}

View File

@@ -0,0 +1,247 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './blocklayered.scss';
/* eslint-disable no-unused-vars, no-alert */
function checkForm() {
let isCategorySelected = false;
let isFilterSelected = false;
$('#categories-treeview input[type=checkbox]').each(function checkCategoriesCheckboxes() {
if ($(this).prop('checked')) {
isCategorySelected = true;
return false;
}
return true;
});
$('.filter_list_item input[type=checkbox]').each(function checkFilterListCheckboxes() {
if ($(this).prop('checked')) {
isFilterSelected = true;
return false;
}
return true;
});
if (!isCategorySelected) {
alert(translations.no_selected_categories);
$('#categories-treeview input[type=checkbox]').first().focus();
return false;
}
if (!isFilterSelected) {
alert(translations.no_selected_filters);
$('#filter_list_item input[type=checkbox]').first().focus();
return false;
}
return true;
}
$(document).ready(() => {
$('.ajaxcall').click(function onAjaxCall() {
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
const type = $(this).attr('rel');
$.ajax({
url: `${this.href}&ajax=1`,
context: this,
dataType: 'json',
cache: 'false',
success() {
this.running = false;
this.restartAllowed = true;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(
type === 'price' ? translations.url_indexation_finished : translations.attribute_indexation_finished,
);
$('#ajax-message-ok').show();
},
error() {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(
type === 'price' ? translations.url_indexation_failed : translations.attribute_indexation_failed,
);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.running = false;
},
});
return false;
});
let totalCount = 0;
$('.ajaxcall-recurcive').each((it, elm) => {
$(elm).click(function onAjaxRecursiveCall(e) {
e.preventDefault();
if (this.cursor === undefined) {
this.cursor = 0;
}
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
$.ajax({
url: `${this.href}&ajax=1&cursor=${this.cursor}`,
context: this,
dataType: 'json',
cache: 'false',
success(res) {
this.running = false;
if (res.result) {
this.cursor = 0;
totalCount = 0;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations.price_indexation_finished);
$('#ajax-message-ok').show();
return;
}
totalCount += parseInt(res.count, 10);
this.cursor = parseInt(res.cursor, 10);
$(this).html(
this.legend + translations.price_indexation_in_progress.replace(
'%s',
`${totalCount}/${res.total}`,
),
);
$(this).click();
},
error(res) {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(translations.price_indexation_failed);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.cursor = 0;
this.running = false;
},
});
return false;
});
});
if (typeof PS_LAYERED_INDEXED !== 'undefined' && PS_LAYERED_INDEXED) {
$('#url-indexe').click();
$('#full-index').click();
}
$('.sortable').sortable({
forcePlaceholderSize: true,
});
$('.filter_list_item input[type=checkbox]').click(function onFilterLickItemCheckboxesClicked() {
const currentSelectedFiltersCount = parseInt($('#selected_filters').html(), 10);
$('#selected_filters').html(
$(this).prop('checked') ? currentSelectedFiltersCount + 1 : currentSelectedFiltersCount - 1,
);
});
if (typeof window.filters !== 'undefined') {
const filters = JSON.parse(window.filters);
let container = null;
let $el;
Object.keys(filters).forEach((filter) => {
$el = $(`#${filter}`);
$el.prop('checked', true);
$('#selected_filters').html(parseInt($('#selected_filters').html(), 10) + 1);
$(`select[name="${filter}_filter_type"]`).val(filters[filter].filter_type);
$(`select[name="${filter}_filter_show_limit"]`).val(filters[filter].filter_show_limit);
if (container === null) {
container = $(`#${filter}`).closest('ul');
$el.closest('li').detach().prependTo(container);
} else {
$el.closest('li').detach().insertAfter(container);
}
container = $el.closest('li');
});
}
});
$(document).on('ready', () => {
const layeredDefaultCategory = $('input[name="ps_layered_filter_by_default_category"]');
layeredDefaultCategory.on('change', function initializeOptions(event) {
const elm = $(this);
if (!elm.prop('checked')) {
return;
}
if (elm.val() === '1') {
$('input[name="ps_layered_full_tree"][value="0"]').prop('checked', true);
$('input[name="ps_layered_full_tree"]').prop('disabled', true);
} else {
$('input[name="ps_layered_full_tree"]').prop('disabled', false);
}
});
layeredDefaultCategory.filter('[value="1"]').trigger('change');
});

View File

@@ -0,0 +1,26 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
class LocalizationException {
constructor(message) {
this.message = message;
this.name = 'LocalizationException';
}
}
export default LocalizationException;

View File

@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import NumberFormatter from './number-formatter';
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
export {
PriceSpecification,
NumberSpecification,
NumberFormatter,
NumberSymbol,
};

View File

@@ -0,0 +1,312 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/**
* These placeholders are used in CLDR number formatting templates.
* They are meant to be replaced by the correct localized symbols in the number formatting process.
*/
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
const escapeRE = require('lodash.escaperegexp');
const CURRENCY_SYMBOL_PLACEHOLDER = '¤';
const DECIMAL_SEPARATOR_PLACEHOLDER = '.';
const GROUP_SEPARATOR_PLACEHOLDER = ',';
const MINUS_SIGN_PLACEHOLDER = '-';
const PERCENT_SYMBOL_PLACEHOLDER = '%';
const PLUS_SIGN_PLACEHOLDER = '+';
class NumberFormatter {
/**
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*/
constructor(specification) {
this.numberSpecification = specification;
}
/**
* Formats the passed number according to specifications.
*
* @param int|float|string number The number to format
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*
* @return string The formatted number
* You should use this this value for display, without modifying it
*/
format(number, specification) {
if (specification !== undefined) {
this.numberSpecification = specification;
}
/*
* We need to work on the absolute value first.
* Then the CLDR pattern will add the sign if relevant (at the end).
*/
const num = Math.abs(number).toFixed(this.numberSpecification.getMaxFractionDigits());
let [majorDigits, minorDigits] = this.extractMajorMinorDigits(num);
majorDigits = this.splitMajorGroups(majorDigits);
minorDigits = this.adjustMinorDigitsZeroes(minorDigits);
// Assemble the final number
let formattedNumber = majorDigits;
if (minorDigits) {
formattedNumber += DECIMAL_SEPARATOR_PLACEHOLDER + minorDigits;
}
// Get the good CLDR formatting pattern. Sign is important here !
const pattern = this.getCldrPattern(number < 0);
formattedNumber = this.addPlaceholders(formattedNumber, pattern);
formattedNumber = this.replaceSymbols(formattedNumber);
formattedNumber = this.performSpecificReplacements(formattedNumber);
return formattedNumber;
}
/**
* Get number's major and minor digits.
*
* Major digits are the "integer" part (before decimal separator),
* minor digits are the fractional part
* Result will be an array of exactly 2 items: [majorDigits, minorDigits]
*
* Usage example:
* list(majorDigits, minorDigits) = this.getMajorMinorDigits(decimalNumber);
*
* @param DecimalNumber number
*
* @return string[]
*/
extractMajorMinorDigits(number) {
// Get the number's major and minor digits.
const result = number.toString().split('.');
const majorDigits = result[0];
const minorDigits = (result[1] === undefined) ? '' : result[1];
return [majorDigits, minorDigits];
}
/**
* Splits major digits into groups.
*
* e.g.: Given the major digits "1234567", and major group size
* configured to 3 digits, the result would be "1 234 567"
*
* @param string majorDigits The major digits to be grouped
*
* @return string The grouped major digits
*/
splitMajorGroups(digit) {
if (!this.numberSpecification.isGroupingUsed()) {
return digit;
}
// Reverse the major digits, since they are grouped from the right.
const majorDigits = digit.split('').reverse();
// Group the major digits.
let groups = [];
groups.push(majorDigits.splice(0, this.numberSpecification.getPrimaryGroupSize()));
while (majorDigits.length) {
groups.push(majorDigits.splice(0, this.numberSpecification.getSecondaryGroupSize()));
}
// Reverse back the digits and the groups
groups = groups.reverse();
const newGroups = [];
groups.forEach((group) => {
newGroups.push(group.reverse().join(''));
});
// Reconstruct the major digits.
return newGroups.join(GROUP_SEPARATOR_PLACEHOLDER);
}
/**
* Adds or remove trailing zeroes, depending on specified min and max fraction digits numbers.
*
* @param string minorDigits Digits to be adjusted with (trimmed or padded) zeroes
*
* @return string The adjusted minor digits
*/
adjustMinorDigitsZeroes(minorDigits) {
let digit = minorDigits;
if (digit.length > this.numberSpecification.getMaxFractionDigits()) {
// Strip any trailing zeroes.
digit = digit.replace(/0+$/, '');
}
if (digit.length < this.numberSpecification.getMinFractionDigits()) {
// Re-add needed zeroes
digit = digit.padEnd(
this.numberSpecification.getMinFractionDigits(),
'0',
);
}
return digit;
}
/**
* Get the CLDR formatting pattern.
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param bool isNegative If true, the negative pattern
* will be returned instead of the positive one
*
* @return string The CLDR formatting pattern
*/
getCldrPattern(isNegative) {
if (isNegative) {
return this.numberSpecification.getNegativePattern();
}
return this.numberSpecification.getPositivePattern();
}
/**
* Replace placeholder number symbols with relevant numbering system's symbols.
*
* @param string number
* The number to process
*
* @return string
* The number with replaced symbols
*/
replaceSymbols(number) {
const symbols = this.numberSpecification.getSymbol();
const map = {};
map[DECIMAL_SEPARATOR_PLACEHOLDER] = symbols.getDecimal();
map[GROUP_SEPARATOR_PLACEHOLDER] = symbols.getGroup();
map[MINUS_SIGN_PLACEHOLDER] = symbols.getMinusSign();
map[PERCENT_SYMBOL_PLACEHOLDER] = symbols.getPercentSign();
map[PLUS_SIGN_PLACEHOLDER] = symbols.getPlusSign();
return this.strtr(number, map);
}
/**
* strtr() for JavaScript
* Translate characters or replace substrings
*/
strtr(str, pairs) {
const substrs = Object.keys(pairs).map(escapeRE);
return str.split(RegExp(`(${substrs.join('|')})`))
.map(part => pairs[part] || part)
.join('');
}
/**
* Add missing placeholders to the number using the passed CLDR pattern.
*
* Missing placeholders can be the percent sign, currency symbol, etc.
*
* e.g. with a currency CLDR pattern:
* - Passed number (partially formatted): 1,234.567
* - Returned number: 1,234.567 ¤
* ("¤" symbol is the currency symbol placeholder)
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param formattedNumber
* Number to process
* @param pattern
* CLDR formatting pattern to use
*
* @return string
*/
addPlaceholders(formattedNumber, pattern) {
/*
* Regex groups explanation:
* # : literal "#" character. Once.
* (,#+)* : any other "#" characters group, separated by ",". Zero to infinity times.
* 0 : literal "0" character. Once.
* (\.[0#]+)* : any combination of "0" and "#" characters groups, separated by '.'.
* Zero to infinity times.
*/
return pattern.replace(/#?(,#+)*0(\.[0#]+)*/, formattedNumber);
}
/**
* Perform some more specific replacements.
*
* Specific replacements are needed when number specification is extended.
* For instance, prices have an extended number specification in order to
* add currency symbol to the formatted number.
*
* @param string formattedNumber
*
* @return mixed
*/
performSpecificReplacements(formattedNumber) {
if (this.numberSpecification instanceof PriceSpecification) {
return formattedNumber
.split(CURRENCY_SYMBOL_PLACEHOLDER)
.join(this.numberSpecification.getCurrencySymbol());
}
return formattedNumber;
}
static build(specifications) {
let symbol;
if (undefined !== specifications.numberSymbols) {
symbol = new NumberSymbol(...specifications.numberSymbols);
} else {
symbol = new NumberSymbol(...specifications.symbol);
}
let specification;
if (specifications.currencySymbol) {
specification = new PriceSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
specifications.currencySymbol,
specifications.currencyCode,
);
} else {
specification = new NumberSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
);
}
return new NumberFormatter(specification);
}
}
export default NumberFormatter;

View File

@@ -0,0 +1,222 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from './exception/localization';
class NumberSymbol {
/**
* NumberSymbolList constructor.
*
* @param string decimal Decimal separator character
* @param string group Digits group separator character
* @param string list List elements separator character
* @param string percentSign Percent sign character
* @param string minusSign Minus sign character
* @param string plusSign Plus sign character
* @param string exponential Exponential character
* @param string superscriptingExponent Superscripting exponent character
* @param string perMille Permille sign character
* @param string infinity The infinity sign. Corresponds to the IEEE infinity bit pattern.
* @param string nan The NaN (Not A Number) sign. Corresponds to the IEEE NaN bit pattern.
*
* @throws LocalizationException
*/
constructor(
decimal,
group,
list,
percentSign,
minusSign,
plusSign,
exponential,
superscriptingExponent,
perMille,
infinity,
nan,
) {
this.decimal = decimal;
this.group = group;
this.list = list;
this.percentSign = percentSign;
this.minusSign = minusSign;
this.plusSign = plusSign;
this.exponential = exponential;
this.superscriptingExponent = superscriptingExponent;
this.perMille = perMille;
this.infinity = infinity;
this.nan = nan;
this.validateData();
}
/**
* Get the decimal separator.
*
* @return string
*/
getDecimal() {
return this.decimal;
}
/**
* Get the digit groups separator.
*
* @return string
*/
getGroup() {
return this.group;
}
/**
* Get the list elements separator.
*
* @return string
*/
getList() {
return this.list;
}
/**
* Get the percent sign.
*
* @return string
*/
getPercentSign() {
return this.percentSign;
}
/**
* Get the minus sign.
*
* @return string
*/
getMinusSign() {
return this.minusSign;
}
/**
* Get the plus sign.
*
* @return string
*/
getPlusSign() {
return this.plusSign;
}
/**
* Get the exponential character.
*
* @return string
*/
getExponential() {
return this.exponential;
}
/**
* Get the exponent character.
*
* @return string
*/
getSuperscriptingExponent() {
return this.superscriptingExponent;
}
/**
* Gert the per mille symbol (often "‰").
*
* @see https://en.wikipedia.org/wiki/Per_mille
*
* @return string
*/
getPerMille() {
return this.perMille;
}
/**
* Get the infinity symbol (often "∞").
*
* @see https://en.wikipedia.org/wiki/Infinity_symbol
*
* @return string
*/
getInfinity() {
return this.infinity;
}
/**
* Get the NaN (not a number) sign.
*
* @return string
*/
getNan() {
return this.nan;
}
/**
* Symbols list validation.
*
* @throws LocalizationException
*/
validateData() {
if (!this.decimal || typeof this.decimal !== 'string') {
throw new LocalizationException('Invalid decimal');
}
if (!this.group || typeof this.group !== 'string') {
throw new LocalizationException('Invalid group');
}
if (!this.list || typeof this.list !== 'string') {
throw new LocalizationException('Invalid symbol list');
}
if (!this.percentSign || typeof this.percentSign !== 'string') {
throw new LocalizationException('Invalid percentSign');
}
if (!this.minusSign || typeof this.minusSign !== 'string') {
throw new LocalizationException('Invalid minusSign');
}
if (!this.plusSign || typeof this.plusSign !== 'string') {
throw new LocalizationException('Invalid plusSign');
}
if (!this.exponential || typeof this.exponential !== 'string') {
throw new LocalizationException('Invalid exponential');
}
if (!this.superscriptingExponent || typeof this.superscriptingExponent !== 'string') {
throw new LocalizationException('Invalid superscriptingExponent');
}
if (!this.perMille || typeof this.perMille !== 'string') {
throw new LocalizationException('Invalid perMille');
}
if (!this.infinity || typeof this.infinity !== 'string') {
throw new LocalizationException('Invalid infinity');
}
if (!this.nan || typeof this.nan !== 'string') {
throw new LocalizationException('Invalid nan');
}
}
}
export default NumberSymbol;

View File

@@ -0,0 +1,170 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSymbol from '../number-symbol';
class NumberSpecification {
/**
* Number specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
) {
this.positivePattern = positivePattern;
this.negativePattern = negativePattern;
this.symbol = symbol;
this.maxFractionDigits = maxFractionDigits;
// eslint-disable-next-line
this.minFractionDigits = maxFractionDigits < minFractionDigits ? maxFractionDigits : minFractionDigits;
this.groupingUsed = groupingUsed;
this.primaryGroupSize = primaryGroupSize;
this.secondaryGroupSize = secondaryGroupSize;
if (!this.positivePattern || typeof this.positivePattern !== 'string') {
throw new LocalizationException('Invalid positivePattern');
}
if (!this.negativePattern || typeof this.negativePattern !== 'string') {
throw new LocalizationException('Invalid negativePattern');
}
if (!this.symbol || !(this.symbol instanceof NumberSymbol)) {
throw new LocalizationException('Invalid symbol');
}
if (typeof this.maxFractionDigits !== 'number') {
throw new LocalizationException('Invalid maxFractionDigits');
}
if (typeof this.minFractionDigits !== 'number') {
throw new LocalizationException('Invalid minFractionDigits');
}
if (typeof this.groupingUsed !== 'boolean') {
throw new LocalizationException('Invalid groupingUsed');
}
if (typeof this.primaryGroupSize !== 'number') {
throw new LocalizationException('Invalid primaryGroupSize');
}
if (typeof this.secondaryGroupSize !== 'number') {
throw new LocalizationException('Invalid secondaryGroupSize');
}
}
/**
* Get symbol.
*
* @return NumberSymbol
*/
getSymbol() {
return this.symbol;
}
/**
* Get the formatting rules for this number (when positive).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getPositivePattern() {
return this.positivePattern;
}
/**
* Get the formatting rules for this number (when negative).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getNegativePattern() {
return this.negativePattern;
}
/**
* Get the maximum number of digits after decimal separator (rounding if needed).
*
* @return int
*/
getMaxFractionDigits() {
return this.maxFractionDigits;
}
/**
* Get the minimum number of digits after decimal separator (fill with "0" if needed).
*
* @return int
*/
getMinFractionDigits() {
return this.minFractionDigits;
}
/**
* Get the "grouping" flag. This flag defines if digits
* grouping should be used when formatting this number.
*
* @return bool
*/
isGroupingUsed() {
return this.groupingUsed;
}
/**
* Get the size of primary digits group in the number.
*
* @return int
*/
getPrimaryGroupSize() {
return this.primaryGroupSize;
}
/**
* Get the size of secondary digits groups in the number.
*
* @return int
*/
getSecondaryGroupSize() {
return this.secondaryGroupSize;
}
}
export default NumberSpecification;

View File

@@ -0,0 +1,109 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSpecification from './number';
/**
* Currency display option: symbol notation.
*/
const CURRENCY_DISPLAY_SYMBOL = 'symbol';
class PriceSpecification extends NumberSpecification {
/**
* Price specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
* @param string currencySymbol Currency symbol of this price (eg. : €)
* @param currencyCode Currency code of this price (e.g.: EUR)
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
currencySymbol,
currencyCode,
) {
super(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
);
this.currencySymbol = currencySymbol;
this.currencyCode = currencyCode;
if (!this.currencySymbol || typeof this.currencySymbol !== 'string') {
throw new LocalizationException('Invalid currencySymbol');
}
if (!this.currencyCode || typeof this.currencyCode !== 'string') {
throw new LocalizationException('Invalid currencyCode');
}
}
/**
* Get type of display for currency symbol.
*
* @return string
*/
static getCurrencyDisplay() {
return CURRENCY_DISPLAY_SYMBOL;
}
/**
* Get the currency symbol
* e.g.: €.
*
* @return string
*/
getCurrencySymbol() {
return this.currencySymbol;
}
/**
* Get the currency ISO code
* e.g.: EUR.
*
* @return string
*/
getCurrencyCode() {
return this.currencyCode;
}
}
export default PriceSpecification;

View File

@@ -0,0 +1,33 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import refreshSliders from './slider';
import {showOverlay, hideOverlay} from './overlay';
$(document).ready(() => {
prestashop.on('updateProductList', () => {
hideOverlay();
refreshSliders();
});
refreshSliders();
prestashop.on('updateFacets', () => {
showOverlay();
});
});

View File

@@ -0,0 +1,56 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
@mixin text-ellipsis() {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#search_filters {
.facet {
.title {
display: flex;
.collapse-icons {
margin-left: auto;
}
}
.facet-title {
@include text-ellipsis();
}
.facet-label {
width: 100%;
text-align: left;
.custom-checkbox,
.custom-radio {
top: -7px;
margin-right: 0;
}
.color {
margin-left: 0;
}
a {
@include text-ellipsis();
}
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import 'jquery-ui-touch-punch';
import './events';
import './slider.scss';
import './facet.scss';

View File

@@ -0,0 +1,43 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './overlay.scss';
const template = `<div class="faceted-overlay">
<div class="overlay__inner">
<div class="overlay__content"><span class="spinner"></span></div>
</div>
</div>`;
function show() {
if ($('.faceted-overlay').length === 1) {
return;
}
$('body').append(template);
}
function hide() {
$('.faceted-overlay').remove();
}
export {
show as showOverlay,
hide as hideOverlay,
};

View File

@@ -0,0 +1,60 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.faceted-overlay {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: fixed;
background-color: rgba(25, 25, 25, 0.5);
z-index: 100;
.overlay__inner {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: absolute;
}
.overlay__content {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.spinner {
width: 75px;
height: 75px;
display: inline-block;
border-width: 2px;
border-color: rgba(255, 255, 255, 0.05);
border-top-color: #fff;
animation: spin 1s infinite linear;
border-radius: 100%;
border-style: solid;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,128 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import getQueryParameters from './urlparser';
import NumberFormatter from '../cldr/number-formatter';
const formatters = {};
const displayLabelBlock = (formatterId, displayBlock, min, max) => {
if (formatters[formatterId] === undefined) {
displayBlock.text(
displayBlock.text().replace(
/([^\d]*)(?:[\d .,]+)([^\d]+)(?:[\d .,]+)(.*)/,
`$1${min}$2${max}$3`,
),
);
} else {
displayBlock.text(
`${formatters[formatterId].format(min)} - ${formatters[formatterId].format(max)}`,
);
}
};
/**
* Refresh facets sliders
*/
const refreshSliders = () => {
$('.faceted-slider').each(function initializeSliders() {
const $el = $(this);
const values = $el.data('slider-values');
const specifications = $el.data('slider-specifications');
if (specifications !== null && specifications !== undefined) {
formatters[$el.data('slider-id')] = NumberFormatter.build(specifications);
}
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
);
$(`#slider-range_${$el.data('slider-id')}`).slider({
range: true,
min: $el.data('slider-min'),
max: $el.data('slider-max'),
values: [
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
],
stop(event, ui) {
const nextEncodedFacetsURL = $el.data('slider-encoded-url');
const urlsSplitted = nextEncodedFacetsURL.split('?');
let queryParams = [];
// Retrieve parameters if exists
if (urlsSplitted.length > 1) {
queryParams = getQueryParameters(urlsSplitted[1]);
}
let found = false;
queryParams.forEach((query) => {
if (query.name === 'q') {
found = true;
}
});
if (!found) {
queryParams.push({name: 'q', value: ''});
}
// Update query parameter
queryParams.forEach((query) => {
if (query.name === 'q') {
// eslint-disable-next-line
query.value += [
query.value.length > 0 ? '/' : '',
$el.data('slider-label'),
'-',
$el.data('slider-unit'),
'-',
ui.values[0],
'-',
ui.values[1],
].join('');
}
});
const requestUrl = [
urlsSplitted[0],
'?',
$.param(queryParams),
].join('');
prestashop.emit(
'updateFacets',
requestUrl,
);
},
slide(event, ui) {
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
ui.values[0],
ui.values[1],
);
},
});
});
};
export default refreshSliders;

View File

@@ -0,0 +1,40 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
#search_filters {
.ui-slider-horizontal {
.ui-slider-handle {
margin-left: -1px;
cursor: pointer;
}
}
.ui-widget-header {
background: #555;
}
.ui-slider {
.ui-slider-handle {
top: -.45em;
width: 0.4em;
background: #fff;
border: 1px solid #555;
}
}
.ui-slider-horizontal {
height: .4em;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
const getQueryParameters = params => params.split('&').map((str) => {
const [key, val] = str.split('=');
return {
name: key,
value: decodeURIComponent(val).replace(/\+/g, ' '),
};
});
export default getQueryParameters;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../');
exit;

View File

@@ -0,0 +1,49 @@
{
"name": "prestashop/ps_facetedsearch",
"description": "PrestaShop module ps_facetedsearch",
"homepage": "https://github.com/PrestaShop/ps_facetedsearch",
"license": "AFL-3.0",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"require": {
"php": ">=5.6",
"doctrine/collections": "^1.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"phpunit/phpunit": "~5.7",
"prestashop/php-coding-standards": "dev-master",
"mockery/mockery": "^1.2"
},
"config": {
"platform": {
"php": "5.6.0"
},
"preferred-install": "dist",
"prepend-autoloader": false
},
"type": "prestashop-module",
"autoload": {
"psr-4": {
"PrestaShop\\Module\\FacetedSearch\\Controller\\": "src/Controller/",
"PrestaShop\\Module\\FacetedSearch\\": "src/",
"PrestaShop\\Module\\FacetedSearch\\Tests\\": "tests/php/FacetedSearch"
},
"classmap": [
"ps_facetedsearch.php"
]
},
"scripts": {
"test": [
"@php -d date.timezone=UTC ./vendor/bin/phpunit -c tests/php/phpunit.xml"
],
"lint": [
"php-cs-fixer fix --no-interaction --dry-run --diff"
]
},
"author": "PrestaShop"
}

3075
modules/ps_facetedsearch/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Faceted search]]></displayName>
<version><![CDATA[3.6.0]]></version>
<description><![CDATA[Displays a block allowing multiple filters.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[front_office_features]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,10 @@
services:
_defaults:
public: true
prestashop.module.ps_facetedsearch.constraint.url_segment_validator:
class: PrestaShop\Module\FacetedSearch\Constraint\UrlSegmentValidator
arguments:
- '@prestashop.adapter.tools'
tags:
- { name: validator.constraint_validator }

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Navigation &agrave; facettes]]></displayName>
<version><![CDATA[3.6.0]]></version>
<description><![CDATA[Affiche un bloc permettant une recherche sur plusieurs filtres &agrave; la fois.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[front_office_features]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

BIN
modules/ps_facetedsearch/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

8860
modules/ps_facetedsearch/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "ps_facetedsearch",
"version": "1.0.0",
"description": "## About",
"private": true,
"directories": {
"test": "tests"
},
"scripts": {
"test": "./node_modules/.bin/mocha --require @babel/register --reporter spec \"./tests/**/*.spec.js\"",
"lint": "eslint --ext .js,.vue .",
"build": "webpack-cli --mode production",
"dev": "webpack-cli --mode development --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/PrestaShop/ps_facetedsearch.git"
},
"keywords": [],
"author": "PrestaShop",
"license": "AFL-3.0",
"bugs": {
"url": "https://github.com/PrestaShop/ps_facetedsearch/issues"
},
"babel": {
"presets": [
"@babel/preset-env"
]
},
"homepage": "https://github.com/PrestaShop/ps_facetedsearch#readme",
"devDependencies": {
"@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6",
"@babel/node": "^7.10.5",
"@babel/preset-env": "^7.11.5",
"@babel/register": "^7.11.5",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^4.2.2",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.2.0",
"eslint-config-prestashop": "0.0.2",
"eslint-plugin-import": "^2.22.0",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^8.1.3",
"node-sass": "^4.14.1",
"sass-loader": "^7.3.1",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
},
"dependencies": {
"jquery-ui-touch-punch": "^0.2.3",
"lodash.escaperegexp": "^4.1.2"
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
die('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$psFacetedsearch = new Ps_Facetedsearch();
$psFacetedsearch->indexAttributes();
$psFacetedsearch->indexFeatures();
$psFacetedsearch->indexAttributeGroup();
echo 1;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
die('Bad token');
}
$psFacetedsearch = new Ps_Facetedsearch();
echo $psFacetedsearch->invalidateLayeredFilterBlockCache();

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
die('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$module = new Ps_Facetedsearch();
if (Tools::getValue('full')) {
echo $module->fullPricesIndexProcess((int) Tools::getValue('cursor'), (int) Tools::getValue('ajax'), true);
} else {
echo $module->pricesIndexProcess((int) Tools::getValue('cursor'), (int) Tools::getValue('ajax'));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if isset($listing.rendered_active_filters)}
{$listing.rendered_active_filters nofilter}
{/if}
{if isset($listing.rendered_facets)}
{$listing.rendered_facets nofilter}
{/if}

View File

@@ -0,0 +1,328 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Doctrine\Common\Collections\ArrayCollection;
abstract class AbstractAdapter implements InterfaceAdapter
{
/**
* @var ArrayCollection
*/
protected $filters;
/**
* @var ArrayCollection
*/
protected $operationsFilters;
/**
* @var ArrayCollection
*/
protected $selectFields;
/**
* @var ArrayCollection
*/
protected $groupFields;
protected $orderField = 'id_product';
protected $orderDirection = 'DESC';
protected $limit = 20;
protected $offset = 0;
/** @var InterfaceAdapter */
protected $initialPopulation = null;
public function __construct()
{
$this->groupFields = new ArrayCollection();
$this->selectFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
}
public function __clone()
{
$this->filters = clone $this->filters;
$this->operationsFilters = clone $this->operationsFilters;
$this->groupFields = clone $this->groupFields;
$this->selectFields = clone $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getInitialPopulation()
{
return $this->initialPopulation;
}
/**
* {@inheritdoc}
*/
public function resetFilter($filterName)
{
if ($this->filters->offsetExists($filterName)) {
$this->filters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilter($filterName)
{
if ($this->operationsFilters->offsetExists($filterName)) {
$this->operationsFilters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilters()
{
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function resetAll()
{
$this->selectFields = new ArrayCollection();
$this->groupFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function getFilter($filterName)
{
if (isset($this->filters[$filterName])) {
return $this->filters[$filterName];
}
return null;
}
/**
* {@inheritdoc}
*/
public function getOrderDirection()
{
return $this->orderDirection;
}
/**
* {@inheritdoc}
*/
public function getOrderField()
{
return $this->orderField;
}
/**
* {@inheritdoc}
*/
public function getGroupFields()
{
return $this->groupFields;
}
/**
* {@inheritdoc}
*/
public function getSelectFields()
{
return $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return $this->filters;
}
/**
* {@inheritdoc}
*/
public function getOperationsFilters()
{
return $this->operationsFilters;
}
/**
* {@inheritdoc}
*/
public function copyFilters(InterfaceAdapter $adapter)
{
$this->filters = clone $adapter->getFilters();
$this->operationsFilters = clone $adapter->getOperationsFilters();
}
/**
* {@inheritdoc}
*/
public function addFilter($filterName, $values, $operator = '=')
{
$filters = $this->filters->get($filterName);
if (!isset($filters[$operator])) {
$filters[$operator] = [];
}
$filters[$operator][] = $values;
$this->filters->set($filterName, $filters);
return $this;
}
/**
* {@inheritdoc}
*/
public function addOperationsFilter($filterName, array $operations = [])
{
$this->operationsFilters->set($filterName, $operations);
return $this;
}
/**
* {@inheritdoc}
*/
public function addSelectField($fieldName)
{
if (!$this->selectFields->contains($fieldName)) {
$this->selectFields->add($fieldName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setSelectFields($selectFields)
{
$this->selectFields = new ArrayCollection($selectFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetSelectField()
{
$this->selectFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function addGroupBy($groupField)
{
$this->groupFields->add($groupField);
return $this;
}
/**
* {@inheritdoc}
*/
public function setGroupFields($groupFields)
{
$this->groupFields = new ArrayCollection($groupFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetGroupBy()
{
$this->groupFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function setFilter($filterName, $value)
{
if ($value !== null) {
$this->filters->set($filterName, $value);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderField($fieldName)
{
$this->orderField = $fieldName;
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderDirection($direction)
{
$this->orderDirection = $direction === 'desc' ? 'desc' : 'asc';
return $this;
}
/**
* {@inheritdoc}
*/
public function setLimit($limit, $offset = 0)
{
$this->limit = $limit ? (int) $limit : null;
$this->offset = (int) $offset;
return $this;
}
}

View File

@@ -0,0 +1,294 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
interface InterfaceAdapter
{
/**
* Set order by field
*
* @param string $fieldName
*
* @return self
*/
public function setOrderField($fieldName);
/**
* Set the order by direction for the given field
*
* @param string $direction
*
* @return self
*/
public function setOrderDirection($direction);
/**
* Set the limit and offset associated with the current search
*
* @param int|null $limit
* @param int $offset
*
* @return self
*/
public function setLimit($limit, $offset = 0);
/**
* Execute the search
*
* @return mixed
*/
public function execute();
/**
* Get the current query
*
* @return string
*/
public function getQuery();
/**
* Get the min & max value of the field filedName associated with the current search
*
* @param string $fieldName
*
* @return mixed
*/
public function getMinMaxValue($fieldName);
/**
* Get the min & max value of the price associated with the current search
*
* @return array
*/
public function getMinMaxPriceValue();
/**
* Return order direction associated with the current search
*
* @return mixed
*/
public function getOrderDirection();
/**
* Return order field associated with the current search
*
* @return mixed
*/
public function getOrderField();
/**
* Return all group fields associated with the current search
*
* @return mixed
*/
public function getGroupFields();
/**
* Return all selected fields associated with the current search
*
* @return mixed
*/
public function getSelectFields();
/**
* Return all the filters associated with the current search
*
* @return mixed
*/
public function getFilters();
/**
* Return all the operations filters associated with the current search
*
* @return mixed
*/
public function getOperationsFilters();
/**
* Return the number of results associated for the current search
*
* @return int
*/
public function count();
/**
* Move the current search into the "initialPopulation"
* This initialPopulation will be used to generate the first derived table 'FROM (SELECT ...)' in the final query
* e.g. : SELECT ... FROM (initialPopulation) p JOIN ....
*/
public function useFiltersAsInitialPopulation();
/**
* Create a new SearchAdapter, keeping the initialPopulation of the current Search
*
* @param string $resetFilter reset this filter inside the initialPopulation
* @param bool $skipInitialPopulation if enable, do not copy the initialPopulation filter
*
* @return InterfaceAdapter
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false);
/**
* Add a new filter with filterName, operator & values to the current search
* If several values are provided with the = operator, it's converted automatically to a IN () in the final query
*
* @param string $filterName
* @param array $values
* @param string $operator
*
* @return self
*/
public function addFilter($filterName, $values, $operator = '=');
/**
* Add a stack of operations with filterName. Operations must contains filterName, values and to the current search
*
* @param string $filterName
* @param array $operations
*
* @return self
*/
public function addOperationsFilter($filterName, array $operations);
/**
* Add fieldName in the current search result
*
* @param string $fieldName
*
* @return self
*/
public function addSelectField($fieldName);
/**
* Returns the number of distinct products, group by fieldName values
*
* @param string $fieldName
*
* @return mixed
*/
public function valueCount($fieldName = null);
/**
* Reset the operations filters
*
* @return self
*/
public function resetOperationsFilters();
/**
* Reset the operations filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetOperationsFilter($filterName);
/**
* Reset the filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetFilter($filterName);
/**
* Return the filter associated with filterName
*
* @param string $filterName
*
* @return mixed
*/
public function getFilter($filterName);
/**
* Set the filterName to the given array value
*
* @param string $filterName
* @param mixed $value
*
* @return mixed
*/
public function setFilter($filterName, $value);
/**
* Return the current initialPopulation
*
* @return self|null
*/
public function getInitialPopulation();
/**
* Return all the filters / groupFields / selectFields
*
* @return self
*/
public function resetAll();
/**
* Copy all the filters & operationsFilters from adapter to the current search
*
* @param InterfaceAdapter $adapter
*/
public function copyFilters(InterfaceAdapter $adapter);
/**
* Set all the select fields
*
* @param array $selectFields
*
* @return self
*/
public function setSelectFields($selectFields);
/**
* Reset all the select fields
*
* @return self
*/
public function resetSelectField();
/**
* Add a group by field
*
* @param string $groupField
*
* @return self
*/
public function addGroupBy($groupField);
/**
* Set the group by fields
*
* @param array $groupFields
*
* @return self
*/
public function setGroupFields($groupFields);
/**
* Reset the group by conditions
*
* @return self
*/
public function resetGroupBy();
}

View File

@@ -0,0 +1,793 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Configuration;
use Context;
use Db;
use Doctrine\Common\Collections\ArrayCollection;
use Product;
use StockAvailable;
class MySQL extends AbstractAdapter
{
/**
* @var string
*/
const TYPE = 'MySQL';
/**
* @var string
*/
const LEFT_JOIN = 'LEFT JOIN';
/**
* @var string
*/
const INNER_JOIN = 'INNER JOIN';
/**
* {@inheritdoc}
*/
public function getMinMaxPriceValue()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['price_min', 'MIN(price_min) as min, MAX(price_max) as max']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [floor((float) $result[0]['min']), ceil((float) $result[0]['max'])];
}
/**
* {@inheritdoc}
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false)
{
$mysqlAdapter = new self();
if ($this->getInitialPopulation() !== null && !$skipInitialPopulation) {
$mysqlAdapter->initialPopulation = clone $this->getInitialPopulation();
if ($resetFilter) {
// Try to reset filter & operations filter
$mysqlAdapter->initialPopulation->resetFilter($resetFilter);
$mysqlAdapter->initialPopulation->resetOperationsFilter($resetFilter);
}
}
return $mysqlAdapter;
}
/**
* {@inheritdoc}
*/
public function execute()
{
return $this->getDatabase()->executeS($this->getQuery());
}
/**
* Construct the final sql query
*
* @return string
*/
public function getQuery()
{
$filterToTableMapping = $this->getFieldMapping();
$orderField = $this->computeOrderByField($filterToTableMapping);
if ($this->getInitialPopulation() === null) {
$referenceTable = _DB_PREFIX_ . 'product';
} else {
$referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')';
}
$query = 'SELECT ';
$selectFields = $this->computeSelectFields($filterToTableMapping);
$whereConditions = $this->computeWhereConditions($filterToTableMapping);
$joinConditions = $this->computeJoinConditions($filterToTableMapping);
$groupFields = $this->computeGroupByFields($filterToTableMapping);
$query .= implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
foreach ($joinConditions as $joinAliasInfos) {
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
}
}
if (!empty($whereConditions)) {
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
}
if ($groupFields) {
$query .= ' GROUP BY ' . implode(', ', $groupFields);
}
if ($orderField) {
$query .= ' ORDER BY ' . $orderField . ' ' . strtoupper($this->getOrderDirection());
}
if ($this->limit !== null) {
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
}
return $query;
}
/**
* Define the mapping between fields and tables
*
* @return array
*/
protected function getFieldMapping()
{
$stockCondition = StockAvailable::addSqlShopRestriction(
null,
null,
'sa'
);
$filterToTableMapping = [
'id_product_attribute' => [
'tableName' => 'product_attribute',
'tableAlias' => 'pa',
'joinCondition' => '(p.id_product = pa.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_attribute' => [
'tableName' => 'product_attribute_combination',
'tableAlias' => 'pac',
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_product_attribute',
],
'id_attribute_group' => [
'tableName' => 'attribute',
'tableAlias' => 'a',
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_attribute',
],
'id_feature' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::INNER_JOIN,
],
'id_shop' => [
'tableName' => 'product_shop',
'tableAlias' => 'ps',
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
'joinType' => self::INNER_JOIN,
],
'id_feature_value' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_category' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'position' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'manufacturer_name' => [
'tableName' => 'manufacturer',
'tableAlias' => 'm',
'fieldName' => 'name',
'joinCondition' => '(p.id_manufacturer = m.id_manufacturer)',
'joinType' => self::INNER_JOIN,
],
'name' => [
'tableName' => 'product_lang',
'tableAlias' => 'pl',
'joinCondition' => '(p.id_product = pl.id_product AND pl.id_shop = ' .
$this->getContext()->shop->id . ' AND pl.id_lang = ' . $this->getContext()->language->id . ')',
'joinType' => self::INNER_JOIN,
],
'nleft' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'nright' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'level_depth' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'out_of_stock' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
],
'quantity' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
'aggregateFunction' => 'SUM',
'aggregateFieldName' => 'quantity',
],
'price_min' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'price_max' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_start' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_end' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'id_group' => [
'tableName' => 'category_group',
'tableAlias' => 'cg',
'joinCondition' => '(cg.id_category = c.id_category)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'nleft',
],
'sales' => [
'tableName' => 'product_sale',
'tableAlias' => 'psales',
'fieldName' => 'quantity',
'fieldAlias' => 'sales',
'joinCondition' => '(psales.id_product = p.id_product)',
'joinType' => self::LEFT_JOIN,
],
];
return $filterToTableMapping;
}
/**
* Compute the orderby fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeOrderByField(array $filterToTableMapping)
{
$orderField = $this->getOrderField();
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
$this->getInitialPopulation()->addSelectField($orderField);
}
// do not try to process the orderField if it already has an alias, or if it's a group function
if (empty($orderField) || strpos($orderField, '.') !== false
|| strpos($orderField, '(') !== false) {
return $orderField;
}
if ($orderField === 'price') {
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
}
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
// put some products at the end of the list
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
return $orderField;
}
/**
* Sort product list: InStock, OOPS with qty 0, OutOfStock
*
* @param string $orderField
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeShowLast($orderField, $filterToTableMapping)
{
// allow only if feature is enabled & it is main product list query
if ($this->getInitialPopulation() === null
|| empty($orderField)
|| !Configuration::get('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
) {
return $orderField;
}
$this->addSelectField('out_of_stock');
// order by out-of-stock last
$computedQuantityField = $this->computeFieldName('quantity', $filterToTableMapping);
$byOutOfStockLast = 'IFNULL(' . $computedQuantityField . ', 0) <= 0';
/**
* Default behaviour when out of stock
* 0 - when deny orders
* 1 - when allow orders
*
* @var int
*/
$isAvailableWhenOutOfStock = (int) Product::isAvailableWhenOutOfStock(2);
// computing values for order by 'allow to order last'
$computedField = $this->computeFieldName('out_of_stock', $filterToTableMapping);
$computedValue = $isAvailableWhenOutOfStock ? 0 : 1;
$computedDirection = $isAvailableWhenOutOfStock ? 'ASC' : 'DESC';
// query: products with zero or less quantity and not available to order go to the end
$byOOPS = str_replace(
[':byOutOfStockLast', ':field', ':value', ':direction'],
[$byOutOfStockLast, $computedField, $computedValue, $computedDirection],
':byOutOfStockLast AND FIELD(:field, :value) :direction'
);
$orderField = $byOutOfStockLast . ', '
. $byOOPS . ', '
. $orderField;
return $orderField;
}
/**
* Add alias to table field name
*
* @param string $fieldName
* @param array $filterToTableMapping
*
* @return string Table Field name with an alias
*/
protected function computeFieldName($fieldName, $filterToTableMapping, $sortByField = false)
{
if (array_key_exists($fieldName, $filterToTableMapping)
&& (
// If the requested order field is in the result, no need to change tableAlias
// unless a fieldName key exists
isset($filterToTableMapping[$fieldName]['fieldName'])
|| $this->getInitialPopulation() === null
|| !$this->getInitialPopulation()->getSelectFields()->contains($fieldName)
)
) {
$joinMapping = $filterToTableMapping[$fieldName];
$fieldName = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $fieldName);
if ($sortByField === false) {
$fieldName .= isset($joinMapping['fieldAlias']) ? ' as ' . $joinMapping['fieldAlias'] : '';
}
if (isset($joinMapping['aggregateFunction'], $joinMapping['aggregateFieldName'])) {
$fieldName = $joinMapping['aggregateFunction'] . '(' . $fieldName . ') as ' . $joinMapping['aggregateFieldName'];
}
} else {
if (strpos($fieldName, '(') === false) {
$fieldName = 'p.' . $fieldName;
}
}
return $fieldName;
}
/**
* Compute the select fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeSelectFields(array $filterToTableMapping)
{
$selectFields = [];
foreach ($this->getSelectFields() as $key => $selectField) {
$selectFields[] = $this->computeFieldName($selectField, $filterToTableMapping);
}
return $selectFields;
}
/**
* Computer the where conditions that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeWhereConditions(array $filterToTableMapping)
{
$whereConditions = [];
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterName => $filterOperations) {
$operationsConditions = [];
foreach ($filterOperations as $operations) {
$conditions = [];
foreach ($operations as $idx => $operation) {
$selectAlias = 'p';
$values = $operation[1];
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
// If index is not the first, append to the table alias for
// multi join
$selectAlias = $joinMapping['tableAlias'] .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
$operation[0] = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $operation[0];
}
if (count($values) === 1) {
$operator = !empty($operation[2]) ? $operation[2] : '=';
$conditions[] = $selectAlias . '.' . $operation[0] . $operator . current($values);
} else {
$conditions[] = $selectAlias . '.' . $operation[0] . ' IN (' . implode(', ', array_map(function ($value) {
return is_numeric($value) ? pSQL($value) : "'" . pSQL($value) . "'";
}, $values)) . ')';
}
}
$operationsConditions[] = '(' . implode(' AND ', $conditions) . ')';
}
++$operationIdx;
if (!empty($operationsConditions)) {
$whereConditions[] = '(' . implode(' OR ', $operationsConditions) . ')';
}
}
foreach ($this->getFilters() as $filterName => $filterContent) {
$selectAlias = 'p';
if (array_key_exists($filterName, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$filterName];
$selectAlias = $joinMapping['tableAlias'];
$filterName = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $filterName;
}
foreach ($filterContent as $operator => $values) {
if (count($values) == 1) {
$values = current($values);
if ($operator === '=') {
if (count($values) == 1) {
$whereConditions[] =
$selectAlias . '.' . $filterName . $operator . "'" . current($values) . "'";
} else {
$whereConditions[] =
$selectAlias . '.' . $filterName . ' IN (' . implode(', ', array_map(function ($value) {
return is_numeric($value) ? pSQL($value) : "'" . pSQL($value) . "'";
}, $values)) . ')';
}
} else {
$orConditions = [];
foreach ($values as $value) {
$orConditions[] = $selectAlias . '.' . $filterName . $operator . $value;
}
$whereConditions[] = implode(' OR ', $orConditions);
}
}
}
}
// if we have several "groups" of the same filter, we need to use the intersect of the matching products
// e.g. : mix of id_feature like Composition & Styles
$idFilteredProducts = null;
foreach ($this->getFilters() as $filterName => $filterContent) {
foreach ($filterContent as $operator => $filterValues) {
if (count($filterValues) <= 1) {
continue;
}
$idTmpFilteredProducts = [];
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->addSelectField('id_product');
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$mysqlAdapter->addFilter($filterName, $filterValues, $operator);
$idProducts = $mysqlAdapter->execute();
foreach ($idProducts as $idProduct) {
$idTmpFilteredProducts[] = $idProduct['id_product'];
}
if ($idFilteredProducts === null) {
$idFilteredProducts = $idTmpFilteredProducts;
} else {
$idFilteredProducts += array_intersect($idFilteredProducts, $idTmpFilteredProducts);
}
if (empty($idFilteredProducts)) {
// set it to 0 to make sure no result will be returned
$idFilteredProducts[] = 0;
break;
}
$whereConditions[] = 'p.id_product IN (' . implode(', ', $idFilteredProducts) . ')';
}
}
return $whereConditions;
}
/**
* Compute the joinConditions needed depending on the fields required in select, where, groupby & orderby fields
*
* @param array $filterToTableMapping
*
* @return ArrayCollection
*/
protected function computeJoinConditions(array $filterToTableMapping)
{
$joinList = new ArrayCollection();
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterOperations) {
foreach ($filterOperations as $operations) {
foreach ($operations as $idx => $operation) {
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
if ($idx !== 0 || $operationIdx !== 0) {
// Index is not the first, append index to tableAlias on joinCondition
$joinMapping['joinCondition'] = preg_replace(
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
'${1}' .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx) .
'.',
$joinMapping['joinCondition']
);
$joinMapping['tableAlias'] .= ($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
}
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
++$operationIdx;
}
$this->addJoinList($joinList, $this->getGroupFields()->getKeys(), $filterToTableMapping);
if (array_key_exists($this->getOrderField(), $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$this->getOrderField()];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
return $joinList;
}
/**
* Helper to add tables infos to the join list.
*
* @param ArrayCollection $joinList
* @param array|ArrayCollection $list
* @param array $filterToTableMapping
*/
private function addJoinList(ArrayCollection $joinList, $list, array $filterToTableMapping)
{
foreach ($list as $field) {
if (array_key_exists($field, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$field];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
/**
* Add the required table infos to the join list, taking care of the dependent tables
*
* @param ArrayCollection $joinList
* @param array $joinMapping
* @param array $filterToTableMapping
*/
private function addJoinConditions(ArrayCollection $joinList, array $joinMapping, array $filterToTableMapping)
{
if (array_key_exists('dependencyField', $joinMapping)) {
$dependencyJoinMapping = $filterToTableMapping[$joinMapping['dependencyField']];
$this->addJoinConditions($joinList, $dependencyJoinMapping, $filterToTableMapping);
}
$joinInfos[$joinMapping['tableAlias']] = [
'tableName' => $joinMapping['tableName'],
'joinCondition' => $joinMapping['joinCondition'],
'joinType' => $joinMapping['joinType'],
];
$joinList->set($joinMapping['tableAlias'] . '_' . $joinMapping['tableName'], $joinInfos);
}
/**
* Compute the groupby condition, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeGroupByFields(array $filterToTableMapping)
{
$groupFields = [];
if ($this->getGroupFields()->isEmpty()) {
return $groupFields;
}
foreach ($this->getGroupFields() as $key => $values) {
if (strpos($values, '.') !== false
|| strpos($values, '(') !== false) {
$groupFields[$key] = $values;
continue;
}
if (array_key_exists($values, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$values];
$groupFields[$key] = $joinMapping['tableAlias'] . '.' . $values;
} else {
$groupFields[$key] = 'p.' . $values;
}
}
return $groupFields;
}
/**
* {@inheritdoc}
*/
public function getMinMaxValue($fieldName)
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['MIN(' . $fieldName . ') as min, MAX(' . $fieldName . ') as max']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [(float) $result[0]['min'], (float) $result[0]['max']];
}
/**
* {@inheritdoc}
*/
public function count()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$result = $mysqlAdapter->valueCount();
return isset($result[0]['c']) ? (int) $result[0]['c'] : 0;
}
/**
* {@inheritdoc}
*/
public function valueCount($fieldName = null)
{
$this->resetGroupBy();
if ($fieldName !== null) {
$this->addGroupBy($fieldName);
$this->addSelectField($fieldName);
}
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
$this->setLimit(null);
$this->setOrderField('');
$this->copyOperationsFilters();
return $this->execute();
}
/**
* {@inheritdoc}
*/
public function useFiltersAsInitialPopulation()
{
$this->setLimit(null);
$this->setOrderField('');
$this->setSelectFields(
[
'id_product',
'id_manufacturer',
'quantity',
'condition',
'weight',
'price',
'sales',
]
);
$this->initialPopulation = clone $this;
$this->resetAll();
$this->addSelectField('id_product');
}
/**
* @return Context
*/
protected function getContext()
{
return Context::getContext();
}
/**
* @return Db
*/
protected function getDatabase()
{
return Db::getInstance();
}
/**
* Copy stock management operation filters
* to make sure quantity is also used
*/
protected function copyOperationsFilters()
{
$initialPopulation = $this->getInitialPopulation();
if (null === $initialPopulation) {
return;
}
$operationsFilters = clone $initialPopulation->getOperationsFilters();
foreach ($operationsFilters as $operationName => $operations) {
$this->addOperationsFilter(
$operationName,
$operations
);
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use Symfony\Component\Validator\Constraint;
class UrlSegment extends Constraint
{
public $message = '%s is invalid.';
/**
* {@inheritdoc}
*/
public function validatedBy()
{
return UrlSegmentValidator::class;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use PrestaShop\PrestaShop\Adapter\Tools;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Class UrlSegmentValidator responsible for validating an URL segment.
*/
class UrlSegmentValidator extends ConstraintValidator
{
/**
* @var Tools
*/
private $tools;
/**
* @param Tools $tools
*/
public function __construct(Tools $tools)
{
$this->tools = $tools;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof UrlSegment) {
throw new UnexpectedTypeException($constraint, UrlSegment::class);
}
if (null === $value || '' === $value) {
return;
}
if (strtolower($value) !== $this->tools->linkRewrite($value)) {
$this->context->buildViolation($constraint->message)
->setTranslationDomain('Admin.Notifications.Error')
->setParameter('%s', $this->formatValue($value))
->addViolation()
;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,467 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use AttributeGroup;
use Category;
use Configuration;
use Context;
use Db;
use Feature;
use FeatureValue;
use Manufacturer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use Tools;
class Converter
{
const WIDGET_TYPE_CHECKBOX = 0;
const WIDGET_TYPE_RADIO = 1;
const WIDGET_TYPE_DROPDOWN = 2;
const WIDGET_TYPE_SLIDER = 3;
const TYPE_ATTRIBUTE_GROUP = 'id_attribute_group';
const TYPE_AVAILABILITY = 'availability';
const TYPE_CATEGORY = 'category';
const TYPE_CONDITION = 'condition';
const TYPE_FEATURE = 'id_feature';
const TYPE_QUANTITY = 'quantity';
const TYPE_MANUFACTURER = 'manufacturer';
const TYPE_PRICE = 'price';
const TYPE_WEIGHT = 'weight';
/**
* @var array
*/
const RANGE_FILTERS = [self::TYPE_PRICE, self::TYPE_WEIGHT];
/**
* @var Context
*/
protected $context;
/**
* @var Db
*/
protected $database;
public function __construct(Context $context, Db $database)
{
$this->context = $context;
$this->database = $database;
}
public function getFacetsFromFilterBlocks(array $filterBlocks)
{
$facets = [];
foreach ($filterBlocks as $filterBlock) {
if (empty($filterBlock)) {
// Empty filter, let's continue
continue;
}
$facet = new Facet();
$facet
->setLabel($filterBlock['name'])
->setProperty('filter_show_limit', $filterBlock['filter_show_limit'])
->setMultipleSelectionAllowed(true);
switch ($filterBlock['type']) {
case self::TYPE_CATEGORY:
case self::TYPE_CONDITION:
case self::TYPE_MANUFACTURER:
case self::TYPE_QUANTITY:
case self::TYPE_ATTRIBUTE_GROUP:
case self::TYPE_FEATURE:
$type = $filterBlock['type'];
if ($filterBlock['type'] === self::TYPE_QUANTITY) {
$type = 'availability';
} elseif ($filterBlock['type'] == self::TYPE_ATTRIBUTE_GROUP) {
$type = 'attribute_group';
$facet->setProperty(self::TYPE_ATTRIBUTE_GROUP, $filterBlock['id_key']);
} elseif ($filterBlock['type'] == self::TYPE_FEATURE) {
$type = 'feature';
$facet->setProperty(self::TYPE_FEATURE, $filterBlock['id_key']);
}
$facet->setType($type);
$filters = [];
foreach ($filterBlock['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id);
if (array_key_exists('checked', $filterArray)) {
$filter->setActive($filterArray['checked']);
}
if (isset($filterArray['color'])) {
if ($filterArray['color'] != '') {
$filter->setProperty('color', $filterArray['color']);
} elseif (file_exists(_PS_COL_IMG_DIR_ . $id . '.jpg')) {
$filter->setProperty('texture', _THEME_COL_DIR_ . $id . '.jpg');
}
}
$filters[] = $filter;
}
if ((int) $filterBlock['filter_show_limit'] !== 0) {
usort($filters, [$this, 'sortFiltersByMagnitude']);
}
$this->hideZeroValuesAndShowLimit($filters, (int) $filterBlock['filter_show_limit']);
if ((int) $filterBlock['filter_show_limit'] !== 0 || $filterBlock['type'] !== self::TYPE_ATTRIBUTE_GROUP) {
usort($filters, [$this, 'sortFiltersByLabel']);
}
// No method available to add all filters
foreach ($filters as $filter) {
$facet->addFilter($filter);
}
break;
case self::TYPE_WEIGHT:
case self::TYPE_PRICE:
$facet
->setType($filterBlock['type'])
->setProperty('min', $filterBlock['min'])
->setProperty('max', $filterBlock['max'])
->setProperty('unit', $filterBlock['unit'])
->setProperty('specifications', $filterBlock['specifications'])
->setMultipleSelectionAllowed(false)
->setProperty('range', true);
$filter = new Filter();
$filter
->setActive($filterBlock['value'] !== null)
->setType($filterBlock['type'])
->setMagnitude($filterBlock['nbr'])
->setProperty('symbol', $filterBlock['unit'])
->setValue($filterBlock['value']);
$facet->addFilter($filter);
break;
}
switch ((int) $filterBlock['filter_type']) {
case self::WIDGET_TYPE_CHECKBOX:
$facet->setMultipleSelectionAllowed(true);
$facet->setWidgetType('checkbox');
break;
case self::WIDGET_TYPE_RADIO:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('radio');
break;
case self::WIDGET_TYPE_DROPDOWN:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('dropdown');
break;
case self::WIDGET_TYPE_SLIDER:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('slider');
break;
}
$facets[] = $facet;
}
return $facets;
}
/**
* @param ProductSearchQuery $query
*
* @return array
*/
public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query)
{
$idShop = (int) $this->context->shop->id;
$idLang = (int) $this->context->language->id;
$idParent = $query->getIdCategory();
if (empty($idParent)) {
$idParent = (int) Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'));
}
$searchFilters = [];
/* Get the filters for the current category */
$filters = $this->database->executeS(
'SELECT type, id_value, filter_show_limit, filter_type FROM ' . _DB_PREFIX_ . 'layered_category
WHERE id_category = ' . (int) $idParent . '
AND id_shop = ' . (int) $idShop . '
GROUP BY `type`, id_value ORDER BY position ASC'
);
$urlSerializer = new URLFragmentSerializer();
$facetAndFiltersLabels = $urlSerializer->unserialize($query->getEncodedFacets());
foreach ($filters as $filter) {
$filterLabel = $this->convertFilterTypeToLabel($filter['type']);
switch ($filter['type']) {
case self::TYPE_MANUFACTURER:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$manufacturers = Manufacturer::getManufacturers(false, $idLang);
$searchFilters[$filter['type']] = [];
foreach ($manufacturers as $manufacturer) {
if (in_array($manufacturer['name'], $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][$manufacturer['name']] = $manufacturer['id_manufacturer'];
}
}
break;
case self::TYPE_QUANTITY:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$quantityArray = [
$this->context->getTranslator()->trans(
'Not available',
[],
'Modules.Facetedsearch.Shop'
) => 0,
$this->context->getTranslator()->trans(
'In stock',
[],
'Modules.Facetedsearch.Shop'
) => 1,
];
$searchFilters[$filter['type']] = [];
foreach ($quantityArray as $quantityName => $quantityId) {
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($quantityName, $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][] = $quantityId;
}
}
break;
case self::TYPE_CONDITION:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$conditionArray = [
$this->context->getTranslator()->trans(
'New',
[],
'Modules.Facetedsearch.Shop'
) => 'new',
$this->context->getTranslator()->trans(
'Used',
[],
'Modules.Facetedsearch.Shop'
) => 'used',
$this->context->getTranslator()->trans(
'Refurbished',
[],
'Modules.Facetedsearch.Shop'
) => 'refurbished',
];
$searchFilters[$filter['type']] = [];
foreach ($conditionArray as $conditionName => $conditionId) {
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($conditionName, $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][] = $conditionId;
}
}
break;
case self::TYPE_FEATURE:
$features = Feature::getFeatures($idLang);
foreach ($features as $feature) {
if ($filter['id_value'] == $feature['id_feature']
&& isset($facetAndFiltersLabels[$feature['name']])
) {
$featureValueLabels = $facetAndFiltersLabels[$feature['name']];
$featureValues = FeatureValue::getFeatureValuesWithLang($idLang, $feature['id_feature']);
foreach ($featureValues as $featureValue) {
if (in_array($featureValue['value'], $featureValueLabels)) {
$searchFilters['id_feature'][$feature['id_feature']][] =
$featureValue['id_feature_value'];
}
}
}
}
break;
case self::TYPE_ATTRIBUTE_GROUP:
$attributesGroup = AttributeGroup::getAttributesGroups($idLang);
foreach ($attributesGroup as $attributeGroup) {
if ($filter['id_value'] == $attributeGroup['id_attribute_group']
&& isset($facetAndFiltersLabels[$attributeGroup['public_name']])
) {
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['public_name']];
$attributes = AttributeGroup::getAttributes($idLang, $attributeGroup['id_attribute_group']);
foreach ($attributes as $attribute) {
if (in_array($attribute['name'], $attributeLabels)) {
$searchFilters['id_attribute_group'][$attributeGroup['id_attribute_group']][] = $attribute['id_attribute'];
}
}
}
}
break;
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if (isset($facetAndFiltersLabels[$filterLabel])) {
$filters = $facetAndFiltersLabels[$filterLabel];
if (isset($filters[1]) && isset($filters[2])) {
$from = $filters[1];
$to = $filters[2];
$searchFilters[$filter['type']][0] = $from;
$searchFilters[$filter['type']][1] = $to;
}
}
break;
case self::TYPE_CATEGORY:
if (isset($facetAndFiltersLabels[$filterLabel])) {
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
$categories = Category::searchByNameAndParentCategoryId($idLang, $queryFilter, $idParent);
if ($categories) {
$searchFilters[$filter['type']][] = $categories['id_category'];
}
}
}
break;
default:
if (isset($facetAndFiltersLabels[$filterLabel])) {
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
$searchFilters[$filter['type']][] = $queryFilter;
}
}
}
}
// Remove all empty selected filters
foreach ($searchFilters as $key => $value) {
switch ($key) {
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if ($value[0] === '' && $value[1] === '') {
unset($searchFilters[$key]);
}
break;
default:
if ($value == '' || $value == []) {
unset($searchFilters[$key]);
}
break;
}
}
return $searchFilters;
}
/**
* Convert filter type to label
*
* @param string $filterType
*/
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case self::TYPE_PRICE:
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_WEIGHT:
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_QUANTITY:
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_MANUFACTURER:
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_CATEGORY:
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_FEATURE:
case self::TYPE_ATTRIBUTE_GROUP:
default:
return null;
}
}
/**
* Hide entries with 0 results
* Hide depending of show limit parameter
*
* @param array $filters
*
* @return array
*/
private function hideZeroValuesAndShowLimit(array $filters, $showLimit)
{
$count = 0;
foreach ($filters as $filter) {
if ($filter->getMagnitude() === 0
|| ($showLimit > 0 && $count >= $showLimit)
) {
$filter->setDisplayed(false);
continue;
}
++$count;
}
return $filters;
}
/**
* Sort filters by magnitude
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByMagnitude(Filter $a, Filter $b)
{
$aMagnitude = $a->getMagnitude();
$bMagnitude = $b->getMagnitude();
if ($aMagnitude == $bMagnitude) {
// Same magnitude, sort by label
return $this->sortFiltersByLabel($a, $b);
}
return $aMagnitude > $bMagnitude ? -1 : +1;
}
/**
* Sort filters by label
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByLabel(Filter $a, Filter $b)
{
return strnatcmp($a->getLabel(), $b->getLabel());
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Configuration;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Product\Search;
use Product;
use Validate;
class Products
{
/**
* Use price tax filter
*
* @var bool
*/
private $psLayeredFilterPriceUsetax;
/**
* Use price rounding
*
* @var bool
*/
private $psLayeredFilterPriceRounding;
/**
* @var AbstractAdapter
*/
private $searchAdapter;
public function __construct(Search $productSearch)
{
$this->searchAdapter = $productSearch->getSearchAdapter();
}
/**
* Get the products associated with the current filters
*
* @param int $productsPerPage
* @param int $page
* @param string $orderBy
* @param string $orderWay
* @param array $selectedFilters
*
* @return array
*/
public function getProductByFilters(
$productsPerPage,
$page,
$orderBy,
$orderWay,
$selectedFilters = []
) {
$orderWay = Validate::isOrderWay($orderWay) ? $orderWay : 'ASC';
$orderBy = Validate::isOrderBy($orderBy) ? $orderBy : 'position';
$this->searchAdapter->setLimit((int) $productsPerPage, ((int) $page - 1) * $productsPerPage);
$this->searchAdapter->setOrderField($orderBy);
$this->searchAdapter->setOrderDirection($orderWay);
$this->searchAdapter->addGroupBy('id_product');
if (isset($selectedFilters['price']) || $orderBy === 'price') {
$this->searchAdapter->addSelectField('id_product');
$this->searchAdapter->addSelectField('price');
$this->searchAdapter->addSelectField('price_min');
$this->searchAdapter->addSelectField('price_max');
}
$matchingProductList = $this->searchAdapter->execute();
$this->pricePostFiltering($matchingProductList, $selectedFilters);
$nbrProducts = $this->searchAdapter->count();
if (empty($nbrProducts)) {
$matchingProductList = [];
}
return [
'products' => $matchingProductList,
'count' => $nbrProducts,
];
}
/**
* Post filter product depending on the price and a few extra config variables
*
* @param array $matchingProductList
* @param array $selectedFilters
*/
private function pricePostFiltering(&$matchingProductList, $selectedFilters)
{
if (!isset($selectedFilters['price'])) {
return;
}
$priceFilter['min'] = (float) ($selectedFilters['price'][0]);
$priceFilter['max'] = (float) ($selectedFilters['price'][1]);
if ($this->psLayeredFilterPriceUsetax === null) {
$this->psLayeredFilterPriceUsetax = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_USETAX');
}
if ($this->psLayeredFilterPriceRounding === null) {
$this->psLayeredFilterPriceRounding = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_ROUNDING');
}
if ($this->psLayeredFilterPriceUsetax || $this->psLayeredFilterPriceRounding) {
$this->filterPrice(
$matchingProductList,
$this->psLayeredFilterPriceUsetax,
$this->psLayeredFilterPriceRounding,
$priceFilter
);
}
}
/**
* Remove products from the product list in case of price postFiltering
*
* @param array $matchingProductList
* @param bool $psLayeredFilterPriceUsetax
* @param bool $psLayeredFilterPriceRounding
* @param array $priceFilter
*/
private function filterPrice(
&$matchingProductList,
$psLayeredFilterPriceUsetax,
$psLayeredFilterPriceRounding,
$priceFilter
) {
/* for this case, price could be out of range, so we need to compute the real price */
foreach ($matchingProductList as $key => $product) {
if (($product['price_min'] < (int) $priceFilter['min'] && $product['price_max'] > (int) $priceFilter['min'])
|| ($product['price_max'] > (int) $priceFilter['max'] && $product['price_min'] < (int) $priceFilter['max'])
) {
$price = Product::getPriceStatic($product['id_product'], $psLayeredFilterPriceUsetax);
if ($psLayeredFilterPriceRounding) {
$price = (int) $price;
}
if ($price < $priceFilter['min'] || $price > $priceFilter['max']) {
// out of range price, exclude the product
unset($matchingProductList[$key]);
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Db;
use PrestaShopDatabaseException;
/**
* Provides form data
*/
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
$isIndexable = true;
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$featureId = (int) $params['id'];
// returns false if request failed.
$queryIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . $featureId
);
$isIndexable = (bool) $queryIndexable;
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . $featureId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
'is_indexable' => $isIndexable,
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Context;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Adds module specific fields to BO form
*/
class FormModifier
{
/**
* @var Context
*/
private $context;
public function __construct(Context $context)
{
$this->context = $context;
}
public function modify(
FormBuilderInterface $formBuilder,
array $data
) {
/**
* @var TranslatorComponent
*/
$translator = $this->context->getTranslator();
$invalidCharsHint = $translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing ' .
'the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by ' .
'choosing the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilder
->add(
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
'data' => $data['url'],
]
)
->add(
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
'data' => $data['meta_title'],
]
)
->add(
'layered_indexable',
SwitchType::class,
[
'required' => false,
'label' => $translator->trans('Indexable', [], 'Modules.Facetedsearch.Admin'),
'help' => $translator->trans(
'Use this attribute in URL generated by the Faceted Search module.',
[],
'Modules.Facetedsearch.Admin'
),
'data' => $data['is_indexable'],
]
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Context;
use Db;
use Ps_Facetedsearch;
abstract class AbstractHook
{
const AVAILABLE_HOOKS = [];
/**
* @var Context
*/
protected $context;
/**
* @var Ps_Facetedsearch
*/
protected $module;
/**
* @var Db
*/
protected $database;
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
$this->context = $module->getContext();
$this->database = $module->getDatabase();
}
/**
* @return array
*/
public function getAvailableHooks()
{
return static::AVAILABLE_HOOKS;
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class Attribute extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeSave',
'displayAttributeForm',
'actionAttributePostProcess',
];
/**
* After save attribute
*
* @param array $params
*/
public function actionAttributeSave(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
(`id_attribute`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_attribute'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete attribute
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute
*
* @param array $params
*/
public function actionAttributePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute form
*
* @param array $params
*/
public function displayAttributeForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('attribute_form.tpl');
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class AttributeGroup extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeGroupSave',
'displayAttributeGroupForm',
'displayAttributeGroupPostProcess',
];
/**
* After save Attributes group
*
* @param array $params
*/
public function actionAttributeGroupSave(array $params)
{
if (empty($params['id_attribute_group']) || Tools::getValue('layered_indexable') === false) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group (`id_attribute_group`, `indexable`)
VALUES (' . (int) $params['id_attribute_group'] . ', ' . (int) Tools::getValue('layered_indexable') . ')'
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
(`id_attribute_group`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_attribute_group'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete attribute group
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute_group'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute group
*
* @param array $params
*/
public function displayAttributeGroupPostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute group form
*
* @param array $params
*
* @return string
*/
public function displayAttributeGroupForm(array $params)
{
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
// Request failed, force $isIndexable
if ($isIndexable === false) {
$isIndexable = true;
}
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('attribute_group_form.tpl');
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Tools;
class Category extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionCategoryAdd',
'actionCategoryDelete',
'actionCategoryUpdate',
];
/**
* Category addition
*
* @param array $params
*/
public function actionCategoryAdd(array $params)
{
$this->module->rebuildLayeredCache([], [(int) $params['category']->id]);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Category update
*
* @param array $params
*/
public function actionCategoryUpdate(array $params)
{
/*
* The category status might (active, inactive) have changed,
* we have to update the layered cache table structure
*/
if (isset($params['category']) && !$params['category']->active) {
$this->cleanAndRebuildCategoryFilters($params);
}
}
/**
* Category deletion
*
* @param array $params
*/
public function actionCategoryDelete(array $params)
{
$this->cleanAndRebuildCategoryFilters($params);
}
/**
* Clean and rebuild category filters
*
* @param array $params
*/
private function cleanAndRebuildCategoryFilters(array $params)
{
$layeredFilterList = $this->database->executeS(
'SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter'
);
foreach ($layeredFilterList as $layeredFilter) {
$data = Tools::unSerialize($layeredFilter['filters']);
if (in_array((int) $params['category']->id, $data['categories'])) {
unset($data['categories'][array_search((int) $params['category']->id, $data['categories'])]);
$this->database->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
SET `filters` = \'' . pSQL(serialize($data)) . '\'
WHERE `id_layered_filter` = ' . (int) $layeredFilter['id_layered_filter']
);
}
}
$this->module->invalidateLayeredFilterBlockCache();
$this->module->buildLayeredCategories();
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Design extends AbstractHook
{
const AVAILABLE_HOOKS = [
'displayLeftColumn',
];
/**
* Force this hook to be called here instance of using WidgetInterface
* because Hook::isHookCallableOn before the instanceof function.
* Which means is_callable always returns true with a __call usage.
*
* @param array $params
*/
public function displayLeftColumn(array $params)
{
return $this->module->fetch('module:ps_facetedsearch/ps_facetedsearch.tpl');
}
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Configuration;
use Language;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormModifier;
use PrestaShopDatabaseException;
use Ps_Facetedsearch;
use Tools;
class Feature extends AbstractHook
{
/**
* @var FormModifier
*/
private $formModifier;
/**
* @var FormDataProvider
*/
private $dataProvider;
public function __construct(Ps_Facetedsearch $module)
{
parent::__construct($module);
$this->formModifier = new FormModifier($module->getContext());
$this->dataProvider = new FormDataProvider($module->getDatabase());
}
const AVAILABLE_HOOKS = [
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
];
/**
* Hook for modifying feature form formBuilder
*
* @param array $params
*
* @throws PrestaShopDatabaseException
*/
public function actionFeatureFormBuilderModifier(array $params)
{
$this->formModifier->modify($params['form_builder'], $this->dataProvider->getData($params));
}
/**
* Hook after create feature.
*
* @since PrestaShop 1.7.7.0
*
* @param array $params
*/
public function actionAfterCreateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after update feature.
*
* @since PrestaShop 1.7.7.0
*
* @param array $params
*/
public function actionAfterUpdateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after delete a feature
*
* @param array $params
*/
public function actionFeatureDelete(array $params)
{
if (empty($params['id_feature'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Hook post process feature
*
* @param array $params
*/
public function displayFeaturePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Hook feature form
*
* @param array $params
*/
public function displayFeatureForm(array $params)
{
if (version_compare(_PS_VERSION_, '1.7.7.0') >= 0) {
return;
}
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
// Request failed, force $isIndexable
if ($isIndexable === false) {
$isIndexable = true;
}
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
if ($result) {
foreach ($result as $data) {
$values[$data['id_lang']] = [
'url_name' => $data['url_name'],
'meta_title' => $data['meta_title'],
];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('feature_form.tpl');
}
/**
* After save feature
*
* @param array $params
*/
public function actionFeatureSave(array $params)
{
if (empty($params['id_feature']) || Tools::getValue('layered_indexable') === false) {
return;
}
$featureId = (int) $params['id_feature'];
$formData = [
'layered_indexable' => Tools::getValue('layered_indexable'),
];
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$seoUrl = Tools::getValue('url_name_' . $langId);
$metaTitle = Tools::getValue('meta_title_' . $langId);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$formData['meta_title'][$langId] = $metaTitle;
$formData['url_name'][$langId] = $seoUrl;
}
$this->save($featureId, $formData);
}
/**
* Saves feature form.
*
* @param int $featureId
* @param array $formData
*
* @since PrestaShop 1.7.7
*/
private function save($featureId, array $formData)
{
$this->cleanLayeredIndexableTables($featureId);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
(`id_feature`, `indexable`)
VALUES (' . (int) $featureId . ', ' . (int) $formData['layered_indexable'] . ')'
);
$defaultLangId = (int) Configuration::get('PS_LANG_DEFAULT');
$query = 'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'(`id_feature`, `id_lang`, `url_name`, `meta_title`) ' .
'VALUES (%d, %d, \'%s\', \'%s\')';
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$metaTitle = pSQL($formData['meta_title'][$langId]);
$seoUrl = $formData['url_name'][$langId];
$name = $formData['name'][$langId] ?: $formData['name'][$defaultLangId];
if (!empty($seoUrl)) {
$seoUrl = pSQL(Tools::link_rewrite($seoUrl));
}
$this->database->execute(
sprintf(
$query,
$featureId,
$langId,
$seoUrl,
$metaTitle
)
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Deletes from layered_indexable_feature and layered_indexable_feature_lang_value by feature id
*
* @param int $featureId
*/
private function cleanLayeredIndexableTables($featureId)
{
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . $featureId
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
WHERE `id_feature` = ' . $featureId
);
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class FeatureValue extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionFeatureValueSave',
'actionFeatureValueDelete',
'displayFeatureValueForm',
'displayFeatureValuePostProcess',
];
/**
* After save feature value
*
* @param array $params
*/
public function actionFeatureValueSave(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
//Removing all indexed language data for this attribute value id
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
(`id_feature_value`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_feature_value'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete Feature value
*
* @param array $params
*/
public function actionFeatureValueDelete(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process feature value
*
* @param array $params
*/
public function displayFeatureValuePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Display feature value form
*
* @param array $params
*
* @return string
*/
public function displayFeatureValueForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('feature_value_form.tpl');
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Product extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionProductSave',
];
/**
* After save product
*
* @param array $params
*/
public function actionProductSave(array $params)
{
if (empty($params['id_product'])) {
return;
}
$this->module->indexProductPrices((int) $params['id_product']);
$this->module->indexAttributes((int) $params['id_product']);
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
class ProductSearch extends AbstractHook
{
const AVAILABLE_HOOKS = [
'productSearchProvider',
];
/**
* Hook project search provider
*
* @param array $params
*
* @return SearchProvider|null
*/
public function productSearchProvider(array $params)
{
$query = $params['query'];
// do something with query,
// e.g. use $query->getIdCategory()
// to choose a template for filters.
// Query is an instance of:
// PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery
if ($query->getIdCategory()) {
$this->context->controller->addJqueryUi('slider');
$this->context->controller->registerStylesheet(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.css'
);
$this->context->controller->registerJavascript(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.js',
['position' => 'bottom', 'priority' => 100]
);
return new SearchProvider(
$this->module,
new Converter(
$this->module->getContext(),
$this->module->getDatabase()
),
new URLSerializer()
);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class SpecificPrice extends AbstractHook
{
/**
* @var array
*/
protected $productsBefore = null;
const AVAILABLE_HOOKS = [
'actionObjectSpecificPriceRuleUpdateBefore',
'actionAdminSpecificPriceRuleControllerSaveAfter',
];
/**
* Before saving a specific price rule
*
* @param array $params
*/
public function actionObjectSpecificPriceRuleUpdateBefore(array $params)
{
if (empty($params['object']->id)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['object'];
$this->productsBefore = $specificPrice->getAffectedProducts();
}
/**
* After saving a specific price rule
*
* @param array $params
*/
public function actionAdminSpecificPriceRuleControllerSaveAfter(array $params)
{
if (empty($params['return']->id) || empty($this->productsBefore)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['return'];
$affectedProducts = array_merge($this->productsBefore, $specificPrice->getAffectedProducts());
foreach ($affectedProducts as $product) {
$this->module->indexProductPrices($product['id_product']);
$this->module->indexAttributes($product['id_product']);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use Ps_Facetedsearch;
/**
* Class works with Hook\AbstractHook instances in order to reduce ps_facetedsearch.php size.
*
* The dispatch method is called from the __call method in the module class.
*/
class HookDispatcher
{
const CLASSES = [
Hook\Attribute::class,
Hook\AttributeGroup::class,
Hook\Category::class,
Hook\Design::class,
Hook\Feature::class,
Hook\FeatureValue::class,
Hook\Product::class,
Hook\ProductSearch::class,
Hook\SpecificPrice::class,
];
/**
* List of available hooks
*
* @var string[]
*/
private $availableHooks = [];
/**
* Hook classes
*
* @var Hook\AbstractHook[]
*/
private $hooks = [];
/**
* Module
*
* @var Ps_Facetedsearch
*/
private $module;
/**
* Init hooks
*
* @param Ps_Facetedsearch $module
*/
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
foreach (self::CLASSES as $hookClass) {
$hook = new $hookClass($this->module);
$this->availableHooks = array_merge($this->availableHooks, $hook->getAvailableHooks());
$this->hooks[] = $hook;
}
}
/**
* Get available hooks
*
* @return string[]
*/
public function getAvailableHooks()
{
return $this->availableHooks;
}
/**
* Find hook and dispatch it
*
* @param string $hookName
* @param array $params
*
* @return mixed
*/
public function dispatch($hookName, array $params = [])
{
$hookName = preg_replace('~^hook~', '', $hookName);
foreach ($this->hooks as $hook) {
if (method_exists($hook, $hookName)) {
return call_user_func([$hook, $hookName], $params);
}
}
// No hook found, render it as a widget
return $this->module->renderWidget($hookName, $params);
}
}

View File

@@ -0,0 +1,285 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Category;
use Configuration;
use Context;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
use Tools;
class Search
{
const STOCK_MANAGEMENT_FILTER = 'with_stock_management';
/**
* @var bool
*/
protected $psStockManagement;
/**
* @var bool
*/
protected $psOrderOutOfStock;
/**
* @var AbstractAdapter
*/
protected $searchAdapter;
/**
* @var Context
*/
protected $context;
/**
* Search constructor.
*
* @param Context $context
* @param string $adapterType
*/
public function __construct(Context $context, $adapterType = MySQLAdapter::TYPE)
{
$this->context = $context;
switch ($adapterType) {
case MySQLAdapter::TYPE:
default:
$this->searchAdapter = new MySQLAdapter();
}
if ($this->psStockManagement === null) {
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
}
if ($this->psOrderOutOfStock === null) {
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
}
}
/**
* @return AbstractAdapter
*/
public function getSearchAdapter()
{
return $this->searchAdapter;
}
/**
* Init the initial population of the search filter
*
* @param array $selectedFilters
*/
public function initSearch($selectedFilters)
{
$homeCategory = Configuration::get('PS_HOME_CATEGORY');
/* If the current category isn't defined or if it's homepage, we have nothing to display */
$idParent = (int) Tools::getValue(
'id_category',
Tools::getValue('id_category_layered', $homeCategory)
);
$parent = new Category((int) $idParent);
$psLayeredFullTree = Configuration::get('PS_LAYERED_FULL_TREE');
if (!$psLayeredFullTree) {
$this->addFilter('id_category', [$parent->id]);
}
$psLayeredFilterByDefaultCategory = Configuration::get('PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY');
if ($psLayeredFilterByDefaultCategory) {
$this->addFilter('id_category_default', [$parent->id]);
}
// Visibility of a product must be in catalog or both (search & catalog)
$this->addFilter('visibility', ['both', 'catalog']);
$this->addSearchFilters(
$selectedFilters,
$psLayeredFullTree ? $parent : null,
(int) $this->context->shop->id
);
}
/**
* @param array $selectedFilters
* @param Category $parent
* @param int $idShop
*/
private function addSearchFilters($selectedFilters, $parent, $idShop)
{
$hasCategory = false;
foreach ($selectedFilters as $key => $filterValues) {
if (!count($filterValues)) {
continue;
}
switch ($key) {
case 'id_feature':
$operationsFilter = [];
foreach ($filterValues as $featureId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_features_' . $featureId,
[[['id_feature_value', $filterValue]]]
);
}
break;
case 'id_attribute_group':
$operationsFilter = [];
foreach ($filterValues as $attributeId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_attributes_' . $attributeId,
[[['id_attribute', $filterValue]]]
);
}
break;
case 'category':
$this->addFilter('id_category', $filterValues);
$this->getSearchAdapter()->resetFilter('id_category_default');
$hasCategory = true;
break;
case 'quantity':
if (count($selectedFilters['quantity']) == 2) {
break;
}
if (!$this->psStockManagement) {
$this->getSearchAdapter()->addFilter('quantity', [0], (!$filterValues[0] ? '<=' : '>'));
break;
}
$operationsFilter = [];
if ($filterValues[0]) {
// Filter for available quantity, we must be able to request
// product with out_of_stock at 1 or 2
// which mean we can buy out of stock products
$operationsFilter[] = [
['quantity', [0], '>='],
['out_of_stock', [1], $this->psOrderOutOfStock ? '>=' : '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
} else {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', !$this->psOrderOutOfStock ? [0, 2] : [0], '='],
];
}
$this->getSearchAdapter()->addOperationsFilter(
self::STOCK_MANAGEMENT_FILTER,
$operationsFilter
);
break;
case 'manufacturer':
$this->addFilter('id_manufacturer', $filterValues);
break;
case 'condition':
if (count($selectedFilters['condition']) == 3) {
break;
}
$this->addFilter('condition', $filterValues);
break;
case 'weight':
if (!empty($selectedFilters['weight'][0]) || !empty($selectedFilters['weight'][1])) {
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][0]],
'>='
);
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][1]],
'<='
);
}
break;
case 'price':
if (isset($selectedFilters['price'])
&& (
$selectedFilters['price'][0] !== '' || $selectedFilters['price'][1] !== ''
)
) {
$this->addPriceFilter(
(float) $selectedFilters['price'][0],
(float) $selectedFilters['price'][1]
);
}
break;
}
}
if (!$hasCategory && $parent !== null) {
$this->getSearchAdapter()->addFilter('nleft', [$parent->nleft], '>=');
$this->getSearchAdapter()->addFilter('nright', [$parent->nright], '<=');
}
$this->getSearchAdapter()->addFilter('id_shop', [$idShop]);
$this->getSearchAdapter()->addGroupBy('id_product');
$this->getSearchAdapter()->useFiltersAsInitialPopulation();
}
/**
* Add a filter with the filterValues extracted from the selectedFilters
*
* @param string $filterName
* @param array $filterValues
*/
public function addFilter($filterName, array $filterValues)
{
$values = [];
foreach ($filterValues as $filterValue) {
if (is_array($filterValue)) {
foreach ($filterValue as $subFilterValue) {
$values[] = (int) $subFilterValue;
}
} else {
$values[] = $filterValue;
}
}
if (!empty($values)) {
$this->getSearchAdapter()->addFilter($filterName, $values);
}
}
/**
* Add a price filter
*
* @param float $minPrice
* @param float $maxPrice
*/
private function addPriceFilter($minPrice, $maxPrice)
{
$this->getSearchAdapter()->addFilter('price_min', [$minPrice], '>=');
$this->getSearchAdapter()->addFilter('price_max', [$maxPrice], '<=');
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Context;
class SearchFactory
{
/**
* Returns an instance of Search for this context
*
* @param Context $context
*
* @return Search
*/
public function build(Context $context)
{
return new Search($context);
}
}

View File

@@ -0,0 +1,512 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\FacetsRendererInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use Ps_Facetedsearch;
use Tools;
class SearchProvider implements FacetsRendererInterface, ProductSearchProviderInterface
{
/**
* @var Ps_Facetedsearch
*/
private $module;
/**
* @var Filters\Converter
*/
private $filtersConverter;
/**
* @var URLSerializer
*/
private $facetsSerializer;
/**
* @var SearchFactory
*/
private $searchFactory;
public function __construct(
Ps_Facetedsearch $module,
Filters\Converter $converter,
URLSerializer $serializer,
SearchFactory $searchFactory = null
) {
$this->module = $module;
$this->filtersConverter = $converter;
$this->facetsSerializer = $serializer;
$this->searchFactory = $searchFactory === null ? new SearchFactory() : $searchFactory;
}
/**
* @return array
*/
private function getAvailableSortOrders()
{
$sortSalesDesc = new SortOrder('product', 'sales', 'desc');
$sortPosAsc = new SortOrder('product', 'position', 'asc');
$sortNameAsc = new SortOrder('product', 'name', 'asc');
$sortNameDesc = new SortOrder('product', 'name', 'desc');
$sortPriceAsc = new SortOrder('product', 'price', 'asc');
$sortPriceDesc = new SortOrder('product', 'price', 'desc');
$translator = $this->module->getTranslator();
return [
$sortSalesDesc->setLabel(
$translator->trans('Best sellers', [], 'Modules.Facetedsearch.Shop')
),
$sortPosAsc->setLabel(
$translator->trans('Relevance', [], 'Modules.Facetedsearch.Shop')
),
$sortNameAsc->setLabel(
$translator->trans('Name, A to Z', [], 'Shop.Theme.Catalog')
),
$sortNameDesc->setLabel(
$translator->trans('Name, Z to A', [], 'Shop.Theme.Catalog')
),
$sortPriceAsc->setLabel(
$translator->trans('Price, low to high', [], 'Shop.Theme.Catalog')
),
$sortPriceDesc->setLabel(
$translator->trans('Price, high to low', [], 'Shop.Theme.Catalog')
),
];
}
/**
* @param ProductSearchContext $context
* @param ProductSearchQuery $query
*
* @return ProductSearchResult
*/
public function runQuery(
ProductSearchContext $context,
ProductSearchQuery $query
) {
$result = new ProductSearchResult();
// extract the filter array from the Search query
$facetedSearchFilters = $this->filtersConverter->createFacetedSearchFiltersFromQuery($query);
$context = $this->module->getContext();
$facetedSearch = $this->searchFactory->build($context);
// init the search with the initial population associated with the current filters
$facetedSearch->initSearch($facetedSearchFilters);
$orderBy = $query->getSortOrder()->toLegacyOrderBy(false);
$orderWay = $query->getSortOrder()->toLegacyOrderWay();
$filterProductSearch = new Filters\Products($facetedSearch);
// get the product associated with the current filter
$productsAndCount = $filterProductSearch->getProductByFilters(
$query->getResultsPerPage(),
$query->getPage(),
$orderBy,
$orderWay,
$facetedSearchFilters
);
$result
->setProducts($productsAndCount['products'])
->setTotalProductsCount($productsAndCount['count'])
->setAvailableSortOrders($this->getAvailableSortOrders());
// now get the filter blocks associated with the current search
$filterBlockSearch = new Filters\Block(
$facetedSearch->getSearchAdapter(),
$context,
$this->module->getDatabase()
);
$idShop = (int) $context->shop->id;
$idLang = (int) $context->language->id;
$idCurrency = (int) $context->currency->id;
$idCountry = (int) $context->country->id;
$idCategory = (int) $query->getIdCategory();
$filterHash = md5(
sprintf(
'%d-%d-%d-%d-%d-%s',
$idShop,
$idCurrency,
$idLang,
$idCategory,
$idCountry,
serialize($facetedSearchFilters)
)
);
$filterBlock = $filterBlockSearch->getFromCache($filterHash);
if (empty($filterBlock)) {
$filterBlock = $filterBlockSearch->getFilterBlock($productsAndCount['count'], $facetedSearchFilters);
$filterBlockSearch->insertIntoCache($filterHash, $filterBlock);
}
$facets = $this->filtersConverter->getFacetsFromFilterBlocks(
$filterBlock['filters']
);
$this->labelRangeFilters($facets);
$this->addEncodedFacetsToFilters($facets);
$this->hideUselessFacets($facets, (int) $result->getTotalProductsCount());
$facetCollection = new FacetCollection();
$nextMenu = $facetCollection->setFacets($facets);
$result->setFacetCollection($nextMenu);
$result->setEncodedFacets($this->facetsSerializer->serialize($facets));
return $result;
}
/**
* Renders an product search result.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderFacets(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters, $displayedFacets, $facetsVar) = $this->prepareActiveFiltersForRender($context, $result);
// No need to render without facets
if (empty($facetsVar)) {
return '';
}
$this->module->getContext()->smarty->assign(
[
'show_quantities' => Configuration::get('PS_LAYERED_SHOW_QTIES'),
'facets' => $facetsVar,
'js_enabled' => $this->module->isAjax(),
'displayedFacets' => $displayedFacets,
'activeFilters' => $activeFilters,
'sort_order' => $result->getCurrentSortOrder()->toString(),
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
);
}
/**
* Renders an product search result of active filters.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderActiveFilters(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters) = $this->prepareActiveFiltersForRender($context, $result);
$this->module->getContext()->smarty->assign(
[
'activeFilters' => $activeFilters,
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/active-filters.tpl'
);
}
/**
* Prepare active filters for renderer.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return array|null
*/
private function prepareActiveFiltersForRender(ProductSearchContext $context, ProductSearchResult $result)
{
$facetCollection = $result->getFacetCollection();
// not all search providers generate menus
if (empty($facetCollection)) {
return null;
}
$facetsVar = array_map(
[$this, 'prepareFacetForTemplate'],
$facetCollection->getFacets()
);
$displayedFacets = [];
$activeFilters = [];
foreach ($facetsVar as $idx => $facet) {
// Remove undisplayed facets
if (!empty($facet['displayed'])) {
$displayedFacets[] = $facet;
}
// Check if a filter is active
foreach ($facet['filters'] as $filter) {
if ($filter['active']) {
$activeFilters[] = $filter;
}
}
}
return [
$activeFilters,
$displayedFacets,
$facetsVar,
];
}
/**
* Converts a Facet to an array with all necessary
* information for templating.
*
* @param Facet $facet
*
* @return array ready for templating
*/
protected function prepareFacetForTemplate(Facet $facet)
{
$facetsArray = $facet->toArray();
foreach ($facetsArray['filters'] as &$filter) {
$filter['facetLabel'] = $facet->getLabel();
if ($filter['nextEncodedFacets'] || $facet->getWidgetType() === 'slider') {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => $filter['nextEncodedFacets'],
'page' => null,
]);
} else {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => null,
]);
}
}
unset($filter);
return $facetsArray;
}
/**
* Add a label associated with the facets
*
* @param array $facets
*/
private function labelRangeFilters(array $facets)
{
foreach ($facets as $facet) {
if (!in_array($facet->getType(), Filters\Converter::RANGE_FILTERS)) {
continue;
}
foreach ($facet->getFilters() as $filter) {
$filterValue = $filter->getValue();
$min = empty($filterValue[0]) ? $facet->getProperty('min') : $filterValue[0];
$max = empty($filterValue[1]) ? $facet->getProperty('max') : $filterValue[1];
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($min),
$unit,
Tools::displayNumber($max),
$unit
)
);
} elseif ($facet->getType() === 'price') {
$filter->setLabel(
sprintf(
'%1$s - %2$s',
Tools::displayPrice($min),
Tools::displayPrice($max)
)
);
}
}
}
}
/**
* This method generates a URL stub for each filter inside the given facets
* and assigns this stub to the filters.
* The URL stub is called 'nextEncodedFacets' because it is used
* to generate the URL of the search once a filter is activated.
*/
private function addEncodedFacetsToFilters(array $facets)
{
// first get the currently active facetFilter in an array
$originalFacetFilters = $this->facetsSerializer->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
foreach ($facets as $facet) {
$activeFacetFilters = $originalFacetFilters;
// If only one filter can be selected, we keep track of
// the current active filter to disable it before generating the url stub
// and not select two filters in a facet that can have only one active filter.
if (!$facet->isMultipleSelectionAllowed() && !$facet->getProperty('range')) {
foreach ($facet->getFilters() as $filter) {
if ($filter->isActive()) {
// we have a currently active filter is the facet, remove it from the facetFilter array
$activeFacetFilters = $this->facetsSerializer->removeFilterFromFacetFilters(
$originalFacetFilters,
$filter,
$facet
);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
// toggle the current filter
if ($filter->isActive() || $facet->getProperty('range')) {
$facetFilters = $this->facetsSerializer->removeFilterFromFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
} else {
$facetFilters = $this->facetsSerializer->addFilterToFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
}
// We've toggled the filter, so the call to serialize
// returns the "URL" for the search when user has toggled
// the filter.
$filter->setNextEncodedFacets(
$urlSerializer->serialize($facetFilters)
);
}
}
}
/**
* Remove the facet when there's only 1 result.
* Keep facet status when it's a slider
*
* @param array $facets
* @param int $totalProducts
*/
private function hideUselessFacets(array $facets, $totalProducts)
{
foreach ($facets as $facet) {
if ($facet->getWidgetType() === 'slider') {
$facet->setDisplayed(
$facet->getProperty('min') != $facet->getProperty('max')
);
continue;
}
$totalFacetProducts = 0;
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0 && $filter->isDisplayed()) {
$totalFacetProducts += $filter->getMagnitude();
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
// There are two filters displayed
$usefulFiltersCount > 1
||
/*
* There is only one fitler and the
* magnitude is different than the
* total products
*/
(
count($facet->getFilters()) === 1
&& $totalFacetProducts < $totalProducts
&& $usefulFiltersCount > 0
)
);
}
}
/**
* Generate a URL corresponding to the current page but
* with the query string altered.
*
* Params from $extraParams that have a null value are stripped,
* and other params are added. Params not in $extraParams are unchanged.
*/
private function updateQueryString(array $extraParams = [])
{
$uriWithoutParams = explode('?', $_SERVER['REQUEST_URI'])[0];
$url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $uriWithoutParams;
$params = [];
$paramsFromUri = '';
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
$paramsFromUri = explode('?', $_SERVER['REQUEST_URI'])[1];
}
parse_str($paramsFromUri, $params);
foreach ($extraParams as $key => $value) {
if (null === $value) {
// Force clear param if null value is passed
unset($params[$key]);
} else {
$params[$key] = $value;
}
}
foreach ($params as $key => $param) {
if (null === $param || '' === $param) {
unset($params[$key]);
}
}
$queryString = str_replace('%2F', '/', http_build_query($params, '', '&'));
return $url . ($queryString ? "?$queryString" : '');
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
class URLSerializer
{
/**
* Add filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, Facet $facet)
{
if ($facet->getProperty('range')) {
$facetValue = $facet->getProperty('values');
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
isset($facetValue[0]) ? $facetValue[0] : $facet->getProperty('min'),
isset($facetValue[1]) ? $facetValue[1] : $facet->getProperty('max'),
];
} else {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
return $facetFilters;
}
/**
* Remove filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet)
{
if ($facet->getProperty('range')) {
unset($facetFilters[$facet->getLabel()]);
} else {
unset($facetFilters[$facet->getLabel()][$facetFilter->getLabel()]);
if (empty($facetFilters[$facet->getLabel()])) {
unset($facetFilters[$facet->getLabel()]);
}
}
return $facetFilters;
}
/**
* Get active facet filters
*
* @return array
*/
public function getActiveFacetFiltersFromFacets(array $facets)
{
$facetFilters = [];
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $facetFilter) {
if (!$facetFilter->isActive()) {
// Filter is not active
continue;
}
if (!$facet->getProperty('range')) {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
} else {
$facetValue = $facetFilter->getValue();
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetValue[0],
$facetValue[1],
];
}
}
}
return $facetFilters;
}
/**
* Serialize facets
*
* @param array $facets
*
* @return string
*/
public function serialize(array $facets)
{
$facetFilters = $this->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
return $urlSerializer->serialize($facetFilters);
}
}

View File

@@ -0,0 +1,174 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import {expect} from 'chai';
import NumberFormatter from '../../../_dev/cldr/number-formatter';
import PriceSpecification from '../../../_dev/cldr/specifications/price';
import NumberSymbol from '../../../_dev/cldr/number-symbol';
describe('NumberFormatter', () => {
let currency;
beforeEach(() => {
const symbol = new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
'∞',
'NaN',
);
currency = new NumberFormatter(
new PriceSpecification(
'¤#,##0.###',
'-¤#,##0.###',
symbol,
3,
0,
true,
3,
3,
'$',
'USD',
),
);
});
describe('extractMajorMinorDigits', () => {
const assertions = [
[10, ['10', '']],
[10.1, ['10', '1']],
[11.12345, ['11', '12345']],
[11.00000, ['11', '']],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]}`, () => {
expect(currency.extractMajorMinorDigits(assertion[0])).to.eql(assertion[1]);
});
});
});
describe('getCldrPattern', () => {
const assertions = [
[false, '¤#,##0.###'],
[true, '-¤#,##0.###'],
];
assertions.forEach((assertion) => {
it(`test isNegative ${assertion[0]}`, () => {
expect(currency.getCldrPattern(assertion[0])).to.eq(assertion[1]);
});
});
});
describe('splitMajorGroups', () => {
const assertions = [
['10', '10'],
['100', '100'],
['1000', '1,000'],
['10000', '10,000'],
['100000', '100,000'],
['1000000', '1,000,000'],
['10000000', '10,000,000'],
['100000000', '100,000,000'],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]} should display ${assertion[1]}`, () => {
expect(currency.splitMajorGroups(assertion[0])).to.eq(assertion[1]);
});
});
});
describe('adjustMinorDigitsZeroes', () => {
const assertions = [
['10000', '10'],
['100', '100'],
['12', '12'],
['120', '120'],
['1271', '1271'],
['1270', '127'],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]} should display ${assertion[1]}`, () => {
currency.numberSpecification.minFractionDigits = 2;
expect(currency.adjustMinorDigitsZeroes(assertion[0])).to.eq(assertion[1]);
});
});
});
describe('addPlaceholders', () => {
const assertions = [
['100,000.13', '¤#,##0.00', '¤100,000.13'],
['100.13', '¤#,##0.00', '¤100.13'],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]} with pattern ${assertion[1]} should display ${assertion[2]}`, () => {
expect(currency.addPlaceholders(assertion[0], assertion[1])).to.eq(assertion[2]);
});
});
});
describe('replaceSymbols', () => {
it('should replace all symbols', () => {
currency.numberSpecification.symbol = new NumberSymbol(
'_',
':)',
';',
'%',
'Moins',
'+',
'E',
'×',
'‰',
'∞',
'NaN',
);
expect(currency.replaceSymbols('¤-10,000,000.13')).to.eq('¤Moins10:)000:)000_13');
});
});
describe('addPlaceholders', () => {
it('should replace currency symbol', () => {
expect(currency.performSpecificReplacements('¤10,000,000.13')).to.eq('$10,000,000.13');
});
});
describe('format', () => {
const assertions = [
['10.3', '$10.300'],
['100.34', '$100.340'],
['1000.345', '$1,000.345'],
['10000.3456', '$10,000.346'],
['100000.512', '$100,000.512'],
['1000000', '$1,000,000.000'],
['10000000', '$10,000,000.000'],
['100000000', '$100,000,000.000'],
['-10.3', '-$10.300'],
['-125.45672', '-$125.457'],
['-125.45627', '-$125.456'],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]} should display ${assertion[1]}`, () => {
expect(currency.format(assertion[0])).to.eq(assertion[1]);
});
});
});
});

View File

@@ -0,0 +1,153 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import {expect} from 'chai';
import NumberSymbol from '../../../_dev/cldr/number-symbol';
describe('NumberSymbol', () => {
describe('validateData', () => {
it('should throw if invalid decimal', () => {
expect(() => { new NumberSymbol(); }).to.throw('Invalid decimal');
});
it('should throw if invalid group', () => {
expect(() => {
new NumberSymbol(
'.',
);
}).to.throw('Invalid group');
});
it('should throw if invalid symbol list', () => {
expect(() => {
new NumberSymbol(
'.',
',',
);
}).to.throw('Invalid symbol list');
});
it('should throw if invalid percentSign', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
);
}).to.throw('Invalid percentSign');
});
it('should throw if invalid minusSign', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
);
}).to.throw('Invalid minusSign');
});
it('should throw if invalid plusSign', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
);
}).to.throw('Invalid plusSign');
});
it('should throw if invalid exponential', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
);
}).to.throw('Invalid exponential');
});
it('should throw if invalid superscriptingExponent', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
);
}).to.throw('Invalid superscriptingExponent');
});
it('should throw if invalid perMille', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
);
}).to.throw('Invalid perMille');
});
it('should throw if invalid infinity', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
);
}).to.throw('Invalid infinity');
});
it('should throw if invalid nan', () => {
expect(() => {
new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
'∞',
);
}).to.throw('Invalid nan');
});
});
});

View File

@@ -0,0 +1,139 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import {expect} from 'chai';
import NumberSpecification from '../../../../_dev/cldr/specifications/number';
import NumberSymbol from '../../../../_dev/cldr/number-symbol';
describe('NumberSpecification', () => {
let symbol;
beforeEach(() => {
symbol = new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
'∞',
'NaN',
);
});
describe('validateData', () => {
it('should throw if invalid positive pattern', () => {
expect(() => {
new NumberSpecification();
}).to.throw('Invalid positivePattern');
});
it('should throw if invalid negative pattern', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
);
}).to.throw('Invalid negativePattern');
});
it('should throw if invalid symbol', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
);
}).to.throw('Invalid symbol');
});
it('should throw if invalid maxFractionDigits', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
);
}).to.throw('Invalid maxFractionDigits');
});
it('should throw if invalid minFractionDigits', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
);
}).to.throw('Invalid minFractionDigits');
});
it('should throw if invalid groupingUsed', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
);
}).to.throw('Invalid groupingUsed');
});
it('should throw if invalid primaryGroupSize', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
false,
);
}).to.throw('Invalid primaryGroupSize');
});
it('should throw if invalid secondaryGroupSize', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
);
}).to.throw('Invalid secondaryGroupSize');
});
it('should not throw if everything is ok', () => {
expect(() => {
new NumberSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
3,
);
}).to.not.throw();
});
});
});

View File

@@ -0,0 +1,172 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import {expect} from 'chai';
import PriceSpecification from '../../../../_dev/cldr/specifications/price';
import NumberSymbol from '../../../../_dev/cldr/number-symbol';
describe('PriceSpecification', () => {
let symbol;
beforeEach(() => {
symbol = new NumberSymbol(
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
'∞',
'NaN',
);
});
describe('validateData', () => {
it('should throw if invalid positive pattern', () => {
expect(() => {
new PriceSpecification();
}).to.throw('Invalid positivePattern');
});
it('should throw if invalid negative pattern', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
);
}).to.throw('Invalid negativePattern');
});
it('should throw if invalid symbol', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
);
}).to.throw('Invalid symbol');
});
it('should throw if invalid maxFractionDigits', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
);
}).to.throw('Invalid maxFractionDigits');
});
it('should throw if invalid minFractionDigits', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
);
}).to.throw('Invalid minFractionDigits');
});
it('should throw if invalid groupingUsed', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
);
}).to.throw('Invalid groupingUsed');
});
it('should throw if invalid primaryGroupSize', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
false,
);
}).to.throw('Invalid primaryGroupSize');
});
it('should throw if invalid secondaryGroupSize', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
);
}).to.throw('Invalid secondaryGroupSize');
});
it('should throw if invalid currencySymbol', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
3,
);
}).to.throw('Invalid currencySymbol');
});
it('should throw if invalid currencyCode', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
3,
'$',
);
}).to.throw('Invalid currencyCode');
});
it('should not throw if everything is ok', () => {
expect(() => {
new PriceSpecification(
'#,##0.###',
'-#,##0.###',
symbol,
3,
0,
true,
3,
3,
'$',
'USD',
);
}).to.not.throw();
});
});
});

View File

@@ -0,0 +1,57 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import {expect} from 'chai';
import getQueryParameters from '../../../_dev/front/urlparser';
describe('getQueryParameters', () => {
const assertions = [
[
'q=%C3%89tat-Nouveau%2FPrix-%E2%82%AC-22-42',
[{
name: 'q',
value: 'État-Nouveau/Prix-€-22-42',
}],
],
[
'q=Prix-%E2%82%AC-22-42/Composition-Carton recycl%C3%A9',
[{
name: 'q',
value: 'Prix-€-22-42/Composition-Carton recyclé',
}],
],
[
'q=Prix-%E2%82%AC-22-42/Composition-Carton recycl%C3%A9&something=thisIsSparta',
[
{
name: 'q',
value: 'Prix-€-22-42/Composition-Carton recyclé',
},
{
name: 'something',
value: 'thisIsSparta',
},
],
],
];
assertions.forEach((assertion) => {
it(`test ${assertion[0]}`, () => {
expect(getQueryParameters(assertion[0])).to.eql(assertion[1]);
});
});
});

View File

@@ -0,0 +1,547 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Adapter;
use Configuration;
use Context;
use Db;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL;
use PrestaShop\Module\FacetedSearch\Product\Search;
use Product;
use stdClass;
use StockAvailable;
class MySQLTest extends MockeryTestCase
{
private $adapter;
protected function setUp()
{
$this->adapter = new MySQL();
$mock = Mockery::mock(StockAvailable::class);
$mock->shouldReceive('addSqlShopRestriction')
->with(null, null, 'sa')
->andReturn('');
StockAvailable::setStaticExpectations($mock);
$stdClass = new stdClass();
$stdClass->shop = new stdClass();
$stdClass->shop->id = 1;
$stdClass->language = new stdClass();
$stdClass->language->id = 2;
$stdClass->country = new stdClass();
$stdClass->country->id = 3;
$stdClass->currency = new stdClass();
$stdClass->currency->id = 4;
$contextMock = Mockery::mock(Context::class);
$contextMock->shouldReceive('getContext')
->andReturn($stdClass);
Context::setStaticExpectations($contextMock);
$configurationMock = Mockery::mock(Configuration::class);
$configurationMock->shouldReceive('get')
->with('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
->andReturn(0);
Configuration::setStaticExpectations($configurationMock);
}
public function testGetEmptyQuery()
{
$this->assertEquals(
'SELECT FROM ps_product p ORDER BY p.id_product DESC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
/**
* @dataProvider oneSelectFieldDataProvider
*/
public function testGetQueryWithOneSelectField($type, $expected)
{
$this->adapter->addSelectField($type);
$this->assertEquals(
$expected,
$this->adapter->getQuery()
);
}
public function testGetMinMaxPriceValue()
{
$dbInstanceMock = Mockery::mock(Db::class)->makePartial();
$dbInstanceMock->shouldReceive('executeS')
->once()
->with('SELECT psi.price_min, MIN(price_min) as min, MAX(price_max) as max FROM ps_product p INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3)')
->andReturn(
[
[
'price_min' => '11',
'min' => '11',
'max' => '35',
],
]
);
$dbMock = Mockery::mock(Db::class)->makePartial();
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
[11.0, 35.0],
$this->adapter->getMinMaxPriceValue()
);
}
public function testGetMinMaxValueForWeight()
{
$dbInstanceMock = Mockery::mock(Db::class);
$dbInstanceMock->shouldReceive('executeS')
->once()
->with('SELECT MIN(weight) as min, MAX(weight) as max FROM ps_product p')
->andReturn(
[
[
'min' => '10',
'max' => '42',
],
]
);
$dbMock = Mockery::mock(Db::class);
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
[10.0, 42.0],
$this->adapter->getMinMaxValue('weight')
);
}
public function testCount()
{
$dbInstanceMock = Mockery::mock(Db::class);
$dbInstanceMock->shouldReceive('executeS')
->once()
->with('SELECT COUNT(DISTINCT p.id_product) c FROM ps_product p')
->andReturn(
[
[
'c' => '100',
],
]
);
$dbMock = Mockery::mock(Db::class);
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
100,
$this->adapter->count()
);
}
public function testValueCount()
{
$dbInstanceMock = Mockery::mock(Db::class);
$dbInstanceMock->shouldReceive('executeS')
->once()
->with('SELECT p.weight, COUNT(DISTINCT p.id_product) c FROM ps_product p GROUP BY p.weight')
->andReturn(
[
[
'weight' => '10',
'c' => '100',
],
]
);
$dbMock = Mockery::mock(Db::class);
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
[
0 => [
'weight' => '10',
'c' => '100',
],
],
$this->adapter->valueCount('weight')
);
}
public function testValueCountWithInitialPopulation()
{
$this->adapter->useFiltersAsInitialPopulation();
$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')
->andReturn(
[
[
'weight' => '10',
'c' => '1000',
],
]
);
$dbMock = Mockery::mock(Db::class);
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
[
0 => [
'weight' => '10',
'c' => '1000',
],
],
$this->adapter->valueCount('weight')
);
}
public function testValueCountWithInitialPopulationAndStockManagement()
{
$this->adapter->useFiltersAsInitialPopulation();
$this->adapter->getInitialPopulation()->addOperationsFilter(
Search::STOCK_MANAGEMENT_FILTER,
[[['quantity', [0], '>=']]]
);
$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')
->andReturn(
[
[
'weight' => '10',
'c' => '1000',
],
]
);
$dbMock = Mockery::mock(Db::class);
$dbMock->shouldReceive('getInstance')
->andReturn($dbInstanceMock);
Db::setStaticExpectations($dbMock);
$this->assertEquals(
[
0 => [
'weight' => '10',
'c' => '1000',
],
],
$this->adapter->valueCount('weight')
);
}
public function testGetQueryWithAllSelectField()
{
$this->adapter->setSelectFields(
[
'id_product',
'id_product_attribute',
'id_attribute',
'id_attribute_group',
'id_feature',
'id_shop',
'id_feature_çvalue',
'id_category',
'name',
'nleft',
'nright',
'level_depth',
'out_of_stock',
'quantity',
'price_min',
'price_max',
'range_start',
'range_end',
'id_group',
'manufacturer_name',
]
);
$this->assertEquals(
'SELECT p.id_product, pa.id_product_attribute, pac.id_attribute, a.id_attribute_group, fp.id_feature, ps.id_shop, p.id_feature_çvalue, cp.id_category, pl.name, c.nleft, c.nright, c.level_depth, sa.out_of_stock, SUM(sa.quantity) as quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end, cg.id_group, 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) INNER JOIN ps_attribute a ON (a.id_attribute = pac.id_attribute) INNER JOIN ps_feature_product fp ON (p.id_product = fp.id_product) INNER JOIN ps_product_shop ps ON (p.id_product = ps.id_product AND ps.id_shop = 1 AND ps.active = TRUE) INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) INNER JOIN ps_product_lang pl ON (p.id_product = pl.id_product AND pl.id_shop = 1 AND pl.id_lang = 2) INNER JOIN ps_category c ON (cp.id_category = c.id_category AND c.active=1) 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_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) LEFT JOIN ps_category_group cg ON (cg.id_category = c.id_category) INNER JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer) ORDER BY p.id_product DESC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
public function testGetQueryWithManyFilters()
{
$this->adapter->setSelectFields(
[
'id_product',
'out_of_stock',
'quantity',
'price_min',
'price_max',
'range_start',
'range_end',
]
);
$this->adapter->addFilter('condition', ['new', 'used'], '=');
$this->adapter->addFilter('weight', [10], '=');
$this->adapter->addFilter('price_min', [10], '>=');
$this->adapter->addFilter('price_min', [100], '<=');
$this->adapter->addFilter('id_product', [2, 20, 200], '=');
$this->assertEquals(
'SELECT p.id_product, sa.out_of_stock, SUM(sa.quantity) as quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end 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) INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) WHERE p.condition IN (\'new\', \'used\') AND p.weight=\'10\' AND psi.price_min>=10 AND psi.price_min<=100 AND p.id_product IN (2, 20, 200) ORDER BY p.id_product DESC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
/**
* @dataProvider getManyOperationsFilters
*/
public function testGetQueryWithManyOperationsFilters($fields, $operationsFilter, $expected)
{
$this->adapter->setSelectFields($fields);
$this->adapter->addOperationsFilter(
'out_of_stock_filter',
$operationsFilter
);
$this->assertEquals(
$expected,
$this->adapter->getQuery()
);
}
public function testGetQueryWithGroup()
{
$this->adapter->addSelectField('id_product');
$this->adapter->addGroupBy('id_product');
$this->adapter->addGroupBy('id_feature_value');
$this->adapter->addGroupBy('p.something_defined_by_me');
$this->assertEquals(
'SELECT p.id_product FROM ps_product p GROUP BY p.id_product, fp.id_feature_value, p.something_defined_by_me ORDER BY p.id_product DESC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
public function testGetQueryWithPriceOrderFieldInDesc()
{
$this->adapter->addSelectField('id_product');
$this->adapter->setOrderField('price');
$this->assertEquals(
'SELECT p.id_product FROM ps_product p ORDER BY psi.price_max DESC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
public function testGetQueryWithPriceOrderFieldInAsc()
{
$this->adapter->addSelectField('id_product');
$this->adapter->setOrderField('price');
$this->adapter->setOrderDirection('asc');
$this->assertEquals(
'SELECT p.id_product FROM ps_product p ORDER BY psi.price_min ASC LIMIT 0, 20',
$this->adapter->getQuery()
);
}
public function testGetQueryWithPriceOrderFieldInAscWithInitialPopulation()
{
$this->adapter->addSelectField('manufacturer_name');
$this->adapter->useFiltersAsInitialPopulation();
$this->adapter->setOrderField('manufacturer_name');
$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) INNER JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer)) p INNER JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer) ORDER BY m.name ASC',
$this->adapter->getQuery()
);
}
public function testGetQueryWithPositionOrderFieldInAscWithInitialPopulation()
{
$this->adapter->addSelectField('id_product');
$this->adapter->useFiltersAsInitialPopulation();
$this->adapter->setOrderField('position');
$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',
$this->adapter->getQuery()
);
}
public function testGetQueryWithComputeShowLastEnabled()
{
$configurationMock = Mockery::mock(Configuration::class);
$configurationMock->shouldReceive('get')
->with('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
->andReturn(true);
Configuration::setStaticExpectations($configurationMock);
$productMock = Mockery::namedMock(Product::class);
$productMock->shouldReceive('isAvailableWhenOutOfStock')
->with(2)
->andReturn(true);
$this->adapter->addSelectField('id_product');
$this->adapter->useFiltersAsInitialPopulation();
$this->adapter->setOrderField('position');
$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',
$this->adapter->getQuery()
);
}
public function testGetQueryWithComputeShowLastEnabledAndDenyOrderOutOfStockProducts()
{
$configurationMock = Mockery::mock(Configuration::class);
$configurationMock->shouldReceive('get')
->with('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
->andReturn(true);
Configuration::setStaticExpectations($configurationMock);
$productMock = Mockery::namedMock(Product::class);
$productMock->shouldReceive('isAvailableWhenOutOfStock')
->with(2)
->andReturn(false);
$this->adapter->addSelectField('id_product');
$this->adapter->useFiltersAsInitialPopulation();
$this->adapter->setOrderField('position');
$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',
$this->adapter->getQuery()
);
}
public function getManyOperationsFilters()
{
return [
[
'fields' => [
'id_product',
'out_of_stock',
'quantity',
'price_min',
'price_max',
'range_start',
'range_end',
],
'operationsFilter' => [
[
['quantity', [0], '>='],
['out_of_stock', [1, 3, 4], '='],
],
[
['quantity', [0], '>'],
['out_of_stock', [1], '='],
],
],
'expected' => 'SELECT p.id_product, sa.out_of_stock, SUM(sa.quantity) as quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end 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) INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) LEFT JOIN ps_stock_available sa_1 ON (p.id_product = sa_1.id_product AND IFNULL(pac.id_product_attribute, 0) = sa_1.id_product_attribute) WHERE ((sa.quantity>=0 AND sa_1.out_of_stock IN (1, 3, 4)) OR (sa.quantity>0 AND sa_1.out_of_stock=1)) ORDER BY p.id_product DESC LIMIT 0, 20',
],
[
'fields' => [
'id_product',
'quantity',
],
'operationsFilter' => [
[
['id_attribute', [2]],
['id_attribute', [4]],
],
[
['quantity', [0], '>'],
['out_of_stock', [1], '='],
],
],
'expected' => 'SELECT p.id_product, SUM(sa.quantity) as quantity 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_attribute_combination pac_1 ON (pa.id_product_attribute = pac_1.id_product_attribute) LEFT JOIN ps_stock_available sa_1 ON (p.id_product = sa_1.id_product AND IFNULL(pac.id_product_attribute, 0) = sa_1.id_product_attribute) WHERE ((pac.id_attribute=2 AND pac_1.id_attribute=4) OR (sa.quantity>0 AND sa_1.out_of_stock=1)) ORDER BY p.id_product DESC LIMIT 0, 20',
],
[
'fields' => [
'id_product',
'quantity',
],
'operationsFilter' => [
[
['id_attribute', [2]],
['id_attribute', [4, 5, 6]],
['id_attribute', [7, 8, 9]],
],
[
['quantity', [0], '>'],
['out_of_stock', [0], '='],
],
],
'expected' => 'SELECT p.id_product, SUM(sa.quantity) as quantity 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_attribute_combination pac_1 ON (pa.id_product_attribute = pac_1.id_product_attribute) LEFT JOIN ps_product_attribute_combination pac_2 ON (pa.id_product_attribute = pac_2.id_product_attribute) LEFT JOIN ps_stock_available sa_1 ON (p.id_product = sa_1.id_product AND IFNULL(pac.id_product_attribute, 0) = sa_1.id_product_attribute) WHERE ((pac.id_attribute=2 AND pac_1.id_attribute IN (4, 5, 6) AND pac_2.id_attribute IN (7, 8, 9)) OR (sa.quantity>0 AND sa_1.out_of_stock=0)) ORDER BY p.id_product DESC LIMIT 0, 20',
],
];
}
public function oneSelectFieldDataProvider()
{
return [
['id_product', 'SELECT p.id_product FROM ps_product p ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_product_attribute', 'SELECT pa.id_product_attribute FROM ps_product p LEFT JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_attribute', 'SELECT pac.id_attribute 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) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_attribute_group', 'SELECT a.id_attribute_group 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) INNER JOIN ps_attribute a ON (a.id_attribute = pac.id_attribute) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_feature', 'SELECT fp.id_feature FROM ps_product p INNER JOIN ps_feature_product fp ON (p.id_product = fp.id_product) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_shop', 'SELECT ps.id_shop FROM ps_product p INNER JOIN ps_product_shop ps ON (p.id_product = ps.id_product AND ps.id_shop = 1 AND ps.active = TRUE) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_feature_value', 'SELECT fp.id_feature_value FROM ps_product p LEFT JOIN ps_feature_product fp ON (p.id_product = fp.id_product) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_category', 'SELECT cp.id_category FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY p.id_product DESC LIMIT 0, 20'],
['position', 'SELECT cp.position FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) ORDER BY p.id_product DESC LIMIT 0, 20'],
['name', 'SELECT pl.name FROM ps_product p INNER JOIN ps_product_lang pl ON (p.id_product = pl.id_product AND pl.id_shop = 1 AND pl.id_lang = 2) ORDER BY p.id_product DESC LIMIT 0, 20'],
['nleft', 'SELECT c.nleft FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) INNER JOIN ps_category c ON (cp.id_category = c.id_category AND c.active=1) ORDER BY p.id_product DESC LIMIT 0, 20'],
['nright', 'SELECT c.nright FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) INNER JOIN ps_category c ON (cp.id_category = c.id_category AND c.active=1) ORDER BY p.id_product DESC LIMIT 0, 20'],
['level_depth', 'SELECT c.level_depth FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) INNER JOIN ps_category c ON (cp.id_category = c.id_category AND c.active=1) ORDER BY p.id_product DESC LIMIT 0, 20'],
['out_of_stock', 'SELECT sa.out_of_stock 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) ORDER BY p.id_product DESC LIMIT 0, 20'],
['quantity', 'SELECT SUM(sa.quantity) as quantity 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) ORDER BY p.id_product DESC LIMIT 0, 20'],
['price_min', 'SELECT psi.price_min FROM ps_product p INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) ORDER BY p.id_product DESC LIMIT 0, 20'],
['price_max', 'SELECT psi.price_max FROM ps_product p INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) ORDER BY p.id_product DESC LIMIT 0, 20'],
['range_start', 'SELECT psi.range_start FROM ps_product p INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) ORDER BY p.id_product DESC LIMIT 0, 20'],
['range_end', 'SELECT psi.range_end FROM ps_product p INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_shop = 1 AND psi.id_currency = 4 AND psi.id_country = 3) ORDER BY p.id_product DESC LIMIT 0, 20'],
['id_group', 'SELECT cg.id_group FROM ps_product p INNER JOIN ps_category_product cp ON (p.id_product = cp.id_product) INNER JOIN ps_category c ON (cp.id_category = c.id_category AND c.active=1) LEFT JOIN ps_category_group cg ON (cg.id_category = c.id_category) ORDER BY p.id_product DESC LIMIT 0, 20'],
['manufacturer_name', 'SELECT m.name FROM ps_product p INNER JOIN ps_manufacturer m ON (p.id_manufacturer = m.id_manufacturer) ORDER BY p.id_product DESC LIMIT 0, 20'],
];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,719 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Filters;
use Configuration;
use Context;
use Db;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use Shop;
use stdClass;
use Tools;
class ConverterTest extends MockeryTestCase
{
/** @var Context */
private $contextMock;
/** @var Db */
private $dbMock;
/** @var Block */
private $converter;
protected function setUp()
{
$mock = Mockery::mock(Configuration::class);
$mock->shouldReceive('get')
->andReturnUsing(function ($arg) {
$valueMap = [
'PS_HOME_CATEGORY' => 1,
'PS_WEIGHT_UNIT' => 'kg',
'PS_STOCK_MANAGEMENT' => '1',
'PS_ORDER_OUT_OF_STOCK' => '0',
'PS_UNIDENTIFIED_GROUP' => '1',
'PS_LAYERED_FILTER_CATEGORY_DEPTH' => 3,
];
return $valueMap[$arg];
});
Configuration::setStaticExpectations($mock);
$this->contextMock = Mockery::mock(Context::class);
$this->contextMock->shop = new stdClass();
$this->contextMock->shop->id = 1;
$this->contextMock->language = new stdClass();
$this->contextMock->language->id = 2;
$this->dbMock = Mockery::mock(Db::class);
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getValue')
->andReturnUsing(function ($arg) {
$valueMap = [
'id_category' => 12,
'id_category_layered' => 11,
];
return $valueMap[$arg];
});
Tools::setStaticExpectations($toolsMock);
$this->shopMock = Mockery::mock(Shop::class);
Shop::setStaticExpectations($this->shopMock);
$this->converter = new Converter($this->contextMock, $this->dbMock);
}
public function testGetFacetsFromFilterBlocksWithEmptyArray()
{
$this->assertEquals(
[],
$this->converter->getFacetsFromFilterBlocks(
[]
)
);
}
/**
* Test different scenario for facets filter
*
* @dataProvider facetsProvider
*/
public function testGetFacetsFromFilterBlocks($filterBlocks, $expected)
{
$this->assertEquals(
$expected,
$this->converter->getFacetsFromFilterBlocks(
[$filterBlocks]
)
);
}
public function facetsProvider()
{
return [
// Empty
[
[],
[],
],
// Categories
[
[
'type_lite' => 'category',
'type' => 'category',
'id_key' => 0,
'name' => 'Categories',
'values' => [
7 => [
'name' => 'Stationery',
'nbr' => '3',
],
8 => [
'name' => 'Home Accessories',
'nbr' => '8',
'checked' => true,
],
],
'filter_show_limit' => '0',
'filter_type' => Converter::WIDGET_TYPE_RADIO,
],
[
Facet::__set_state(
[
'label' => 'Categories',
'type' => 'category',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
],
'filters' => [
Filter::__set_state(
[
'label' => 'Home Accessories',
'type' => 'category',
'active' => true,
'displayed' => true,
'properties' => [],
'magnitude' => 8,
'value' => 8,
'nextEncodedFacets' => [],
]
),
1 => Filter::__set_state(
[
'label' => 'Stationery',
'type' => 'category',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 3,
'value' => 7,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => false,
'widgetType' => 'radio',
]
),
],
],
// Attribute group
[
[
'type_lite' => 'id_attribute_group',
'type' => 'id_attribute_group',
'id_key' => '2',
'name' => 'Color',
'is_color_group' => true,
'values' => [
8 => [
'name' => 'White',
'nbr' => '3',
'url_name' => null,
'meta_title' => null,
'color' => '#ffffff',
],
11 => [
'name' => 'Black',
'nbr' => '3',
'url_name' => null,
'meta_title' => null,
'color' => '#434A54',
],
12 => [
'name' => 'Weird',
'nbr' => '3',
'url_name' => null,
'meta_title' => null,
'color' => '',
],
],
'url_name' => null,
'meta_title' => null,
'filter_show_limit' => '0',
'filter_type' => Converter::WIDGET_TYPE_DROPDOWN,
],
[
Facet::__set_state(
[
'label' => 'Color',
'type' => 'attribute_group',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
'id_attribute_group' => '2',
],
'filters' => [
Filter::__set_state(
[
'label' => 'White',
'type' => 'attribute_group',
'active' => false,
'displayed' => true,
'properties' => [
'color' => '#ffffff',
],
'magnitude' => 3,
'value' => 8,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Black',
'type' => 'attribute_group',
'active' => false,
'displayed' => true,
'properties' => [
'color' => '#434A54',
],
'magnitude' => 3,
'value' => 11,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Weird',
'type' => 'attribute_group',
'active' => false,
'displayed' => true,
'properties' => [
'texture' => '/theme/12.jpg',
],
'magnitude' => 3,
'value' => 12,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => false,
'widgetType' => 'dropdown',
]
),
],
],
// Feature values
[
[
'type_lite' => 'id_feature',
'type' => 'id_feature',
'id_key' => '2',
'values' => [
5 => [
'nbr' => '2',
'name' => '2',
'url_name' => null,
'meta_title' => null,
],
6 => [
'nbr' => '2',
'name' => '1',
'url_name' => null,
'meta_title' => null,
],
7 => [
'nbr' => '2',
'name' => '2.2',
'url_name' => null,
'meta_title' => null,
],
8 => [
'nbr' => '2',
'name' => '2.1',
'url_name' => null,
'meta_title' => null,
],
9 => [
'nbr' => '3',
'name' => 'Removable cover',
'url_name' => null,
'meta_title' => null,
],
10 => [
'nbr' => '3',
'name' => '120 pages',
'url_name' => null,
'meta_title' => null,
],
],
'name' => 'Property',
'url_name' => null,
'meta_title' => null,
'filter_show_limit' => '0',
'filter_type' => '0',
],
[
Facet::__set_state(
[
'label' => 'Property',
'type' => 'feature',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
'id_feature' => '2',
],
'filters' => [
Filter::__set_state(
[
'label' => '1',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 6,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => '2',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 5,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => '2.1',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 8,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => '2.2',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 7,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => '120 pages',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 3,
'value' => 10,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Removable cover',
'type' => 'feature',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 3,
'value' => 9,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => true,
'widgetType' => 'checkbox',
]
),
],
],
// Quantity
[
[
'type_lite' => 'quantity',
'type' => 'quantity',
'id_key' => 0, 'name' => 'Availability',
'values' => [
0 => [
'name' => 'Not available',
'nbr' => 0,
],
1 => [
'name' => 'In stock',
'nbr' => 11,
],
],
'filter_show_limit' => '0',
'filter_type' => '0',
],
[
Facet::__set_state(
[
'label' => 'Availability',
'type' => 'availability',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
],
'filters' => [
Filter::__set_state(
[
'label' => 'In stock',
'type' => 'availability',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 11,
'value' => 1,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Not available',
'type' => 'availability',
'active' => false,
'displayed' => false,
'properties' => [],
'magnitude' => 0,
'value' => 0,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => true,
'widgetType' => 'checkbox',
]
),
],
],
// Manufacturer
[
[
'type_lite' => 'manufacturer',
'type' => 'manufacturer',
'id_key' => 0, 'name' => 'Brand',
'values' => [
1 => [
'name' => 'Studio Design',
'nbr' => '7',
],
2 => [
'name' => 'Graphic Corner',
'nbr' => '3',
],
],
'filter_show_limit' => '0',
'filter_type' => '0',
],
[
Facet::__set_state(
[
'label' => 'Brand',
'type' => 'manufacturer',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
],
'filters' => [
Filter::__set_state(
[
'label' => 'Graphic Corner',
'type' => 'manufacturer',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 3,
'value' => 2,
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Studio Design',
'type' => 'manufacturer',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 7,
'value' => 1,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => true,
'widgetType' => 'checkbox',
]
),
],
],
// Condition
[
[
'type_lite' => 'condition',
'type' => 'condition',
'id_key' => 0, 'name' => 'Condition',
'values' => [
'new' => [
'name' => 'New',
'nbr' => '11',
],
'used' => [
'name' => 'Used',
'nbr' => 0,
],
'refurbished' => [
'name' => 'Refurbished',
'nbr' => 0,
],
],
'filter_show_limit' => '0',
'filter_type' => '0',
],
[
Facet::__set_state(
[
'label' => 'Condition',
'type' => 'condition',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
],
'filters' => [
Filter::__set_state(
[
'label' => 'New',
'type' => 'condition',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 11,
'value' => 'new',
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Refurbished',
'type' => 'condition',
'active' => false,
'displayed' => false,
'properties' => [],
'magnitude' => 0,
'value' => 'refurbished',
'nextEncodedFacets' => [],
]
),
Filter::__set_state(
[
'label' => 'Used',
'type' => 'condition',
'active' => false,
'displayed' => false,
'properties' => [],
'magnitude' => 0,
'value' => 'used',
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => true,
'widgetType' => 'checkbox',
]
),
],
],
// Price
[
[
'type_lite' => 'price',
'type' => 'price',
'id_key' => 0, 'name' => 'Price',
'max' => 35.0, 'min' => 11.0, 'unit' => '$',
'specifications' => [
'symbol' => [
0 => '.',
1 => ',',
2 => ';',
3 => '%',
4 => '-',
5 => '+',
6 => 'E',
7 => '×',
8 => '‰',
9 => '∞',
10 => 'NaN',
],
'currencyCode' => 'USD',
'currencySymbol' => '$',
'positivePattern' => '¤#,##0.00',
'negativePattern' => '-¤#,##0.00',
'maxFractionDigits' => 2,
'minFractionDigits' => 2,
'groupingUsed' => true,
'primaryGroupSize' => 3,
'secondaryGroupSize' => 3,
],
'filter_show_limit' => 0,
'filter_type' => 3, 'nbr' => '11',
'value' => null,
],
[
Facet::__set_state(
[
'label' => 'Price',
'type' => 'price',
'displayed' => true,
'properties' => [
'filter_show_limit' => 0,
'min' => 11.0,
'max' => 35.0,
'unit' => '$',
'specifications' => [
'symbol' => [
0 => '.',
1 => ',',
2 => ';',
3 => '%',
4 => '-',
5 => '+',
6 => 'E',
7 => '×',
8 => '‰',
9 => '∞',
10 => 'NaN',
],
'currencyCode' => 'USD',
'currencySymbol' => '$',
'positivePattern' => '¤#,##0.00',
'negativePattern' => '-¤#,##0.00',
'maxFractionDigits' => 2,
'minFractionDigits' => 2,
'groupingUsed' => true,
'primaryGroupSize' => 3,
'secondaryGroupSize' => 3,
],
'range' => true,
],
'filters' => [
Filter::__set_state(
[
'label' => '',
'type' => 'price',
'active' => false,
'displayed' => true,
'properties' => [
'symbol' => '$',
],
'magnitude' => 11,
'value' => null,
'nextEncodedFacets' => [],
]
),
],
'multipleSelectionAllowed' => false,
'widgetType' => 'slider',
]
),
],
],
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests;
use Context;
use Db;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\HookDispatcher;
use Ps_Facetedsearch;
class HookDispatcherTest extends MockeryTestCase
{
private $module;
private $dispatcher;
protected function setUp()
{
$this->module = Mockery::mock(Ps_Facetedsearch::class);
$contextMock = Mockery::mock(Context::class);
$dbMock = Mockery::mock(Db::class);
$this->module->shouldReceive('getDatabase')
->andReturn($dbMock);
$this->module->shouldReceive('getContext')
->andReturn($contextMock);
$this->dispatcher = new HookDispatcher($this->module);
}
public function testGetAvailableHooks()
{
$this->assertCount(27, $this->dispatcher->getAvailableHooks());
$this->assertEquals(
[
'actionAttributeGroupDelete',
'actionAttributeSave',
'displayAttributeForm',
'actionAttributePostProcess',
'actionAttributeGroupDelete',
'actionAttributeGroupSave',
'displayAttributeGroupForm',
'displayAttributeGroupPostProcess',
'actionCategoryAdd',
'actionCategoryDelete',
'actionCategoryUpdate',
'displayLeftColumn',
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
'actionFeatureValueSave',
'actionFeatureValueDelete',
'displayFeatureValueForm',
'displayFeatureValuePostProcess',
'actionProductSave',
'productSearchProvider',
'actionObjectSpecificPriceRuleUpdateBefore',
'actionAdminSpecificPriceRuleControllerSaveAfter',
],
$this->dispatcher->getAvailableHooks()
);
}
public function testDispatchUnfoundHook()
{
$this->module->shouldReceive('renderWidget')
->once()
->with('ThisHookDoesNotExists', [])
->andReturn('');
$this->assertEquals('', $this->dispatcher->dispatch('ThisHookDoesNotExists'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\PrestaShop\Core\Product\Search;
interface FacetsRendererInterface
{
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\PrestaShop\Core\Product\Search;
interface ProductSearchProviderInterface
{
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\PrestaShop\Core\Module;
interface WidgetInterface
{
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\PrestaShop\Core\Product\Search;
/**
* We call a facet a set of filters combined with logical operators.
*/
class Facet
{
/**
* @var string the facet label
*/
private $label = '';
/**
* @var string the facet type
*/
private $type = '';
/**
* @var bool if true, the facet is displayed
*/
private $displayed = true;
/**
* @var array the facet properties
*/
private $properties = [];
/**
* @var array the facet filters
*/
private $filters = [];
/**
* @var bool if true, allows the multiple selection
*/
private $multipleSelectionAllowed = true;
/**
* @var string the widget type
*/
private $widgetType = 'radio';
/**
* @return array an array representation of the facet
*/
public function toArray()
{
return [
'label' => $this->label,
'displayed' => $this->displayed,
'type' => $this->type,
'properties' => $this->properties,
'filters' => array_map(function (Filter $filter) {
return $filter->toArray();
}, $this->filters),
'multipleSelectionAllowed' => $this->multipleSelectionAllowed,
'widgetType' => $this->widgetType,
];
}
/**
* @param string $label the facet label
*
* @return $this
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* @return string the facet label
*/
public function getLabel()
{
return $this->label;
}
/**
* @param string $type the facet type
*
* @return $this
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* @return string the facet type
*/
public function getType()
{
return $this->type;
}
/**
* @param string $name the facet property name
* @param mixed $value the facet property value
*
* @return $this
*/
public function setProperty($name, $value)
{
$this->properties[$name] = $value;
return $this;
}
/**
* @param string $name the facet property name
*
* @return mixed|null
*/
public function getProperty($name)
{
if (!array_key_exists($name, $this->properties)) {
return null;
}
return $this->properties[$name];
}
/**
* @param Filter $filter the facet filter
*
* @return $this
*/
public function addFilter(Filter $filter)
{
$this->filters[] = $filter;
return $this;
}
/**
* @return array the list of facet filters
*/
public function getFilters()
{
return $this->filters;
}
/**
* @param bool $isAllowed allows/disallows the multiple selection
*
* @return $this
*/
public function setMultipleSelectionAllowed($isAllowed = true)
{
$this->multipleSelectionAllowed = $isAllowed;
return $this;
}
/**
* @return bool returns true if multiple selection is allowed
*/
public function isMultipleSelectionAllowed()
{
return $this->multipleSelectionAllowed;
}
/**
* @param bool $displayed sets the display of the facet
*
* @return $this
*/
public function setDisplayed($displayed = true)
{
$this->displayed = $displayed;
return $this;
}
/**
* @return bool returns true if the facet is displayed
*/
public function isDisplayed()
{
return $this->displayed;
}
/**
* @param string $widgetType sets the widget type of the facet
*
* @return $this
*/
public function setWidgetType($widgetType)
{
$this->widgetType = $widgetType;
return $this;
}
/**
* @return string returns the facet widget type
*/
public function getWidgetType()
{
return $this->widgetType;
}
/**
* Functions created for testing
*/
public function setProperties(array $data)
{
$this->properties = $data;
}
public function setFilters(array $data)
{
$this->filters = $data;
}
public static function __set_state($data)
{
$obj = new self();
$obj->setLabel($data['label']);
$obj->setDisplayed($data['displayed']);
$obj->setType($data['type']);
$obj->setProperties($data['properties']);
$obj->setFilters($data['filters']);
$obj->setMultipleSelectionAllowed($data['multipleSelectionAllowed']);
$obj->setWidgetType($data['widgetType']);
return $obj;
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\PrestaShop\Core\Product\Search;
class Filter
{
/**
* @var string the filter label
*/
private $label = '';
/**
* @var string internal type, used by query logic
*/
private $type = '';
/**
* @var bool whether or not the filter is used in the query
*/
private $active = false;
/**
* @var bool whether or not the filter is displayed
*/
private $displayed = true;
/**
* @var array the filter properties
*/
private $properties = [];
/**
* @var int the filter magnitude
*/
private $magnitude = 0;
/**
* @var mixed the filter value
*/
private $value;
/**
* @var array the filter next encoded facets
*/
private $nextEncodedFacets = [];
/**
* @return array an array representation of the filter
*/
public function toArray()
{
return [
'label' => $this->label,
'type' => $this->type,
'active' => $this->active,
'displayed' => $this->displayed,
'properties' => $this->properties,
'magnitude' => $this->magnitude,
'value' => $this->value,
'nextEncodedFacets' => $this->nextEncodedFacets,
];
}
/**
* @param string $label the filter label
*
* @return $this
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* @return string the filter label
*/
public function getLabel()
{
return $this->label;
}
/**
* @param string $type the filter type
*
* @return $this
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* @return string the filter type
*/
public function getType()
{
return $this->type;
}
/**
* @param string $name the filter property name
* @param mixed $value the filter property value
*
* @return $this
*/
public function setProperty($name, $value)
{
$this->properties[$name] = $value;
return $this;
}
/**
* @param string $name the filter property name
*
* @return mixed|null
*/
public function getProperty($name)
{
if (!array_key_exists($name, $this->properties)) {
return null;
}
return $this->properties[$name];
}
/**
* @param $value
*
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param int $magnitude the filter magnitude
*
* @return $this
*/
public function setMagnitude($magnitude)
{
$this->magnitude = (int) $magnitude;
return $this;
}
/**
* @return int the filter magnitude
*/
public function getMagnitude()
{
return $this->magnitude;
}
/**
* @param bool $active sets the activation of the filter
*
* @return $this
*/
public function setActive($active = true)
{
$this->active = $active;
return $this;
}
/**
* @return bool returns true if the filter is active
*/
public function isActive()
{
return $this->active;
}
/**
* @param bool $displayed sets the display of the filter
*
* @return $this
*/
public function setDisplayed($displayed = true)
{
$this->displayed = $displayed;
return $this;
}
/**
* @return bool returns true if the filter is displayed
*/
public function isDisplayed()
{
return $this->displayed;
}
/**
* @param $nextEncodedFacets
*
* @return $this
*/
public function setNextEncodedFacets($nextEncodedFacets)
{
$this->nextEncodedFacets = $nextEncodedFacets;
return $this;
}
/**
* @return array
*/
public function getNextEncodedFacets()
{
return $this->nextEncodedFacets;
}
/**
* Functions created for testing
*/
public function setProperties(array $data)
{
$this->properties = $data;
}
public static function __set_state($data)
{
$obj = new self();
$obj->setLabel($data['label']);
$obj->setDisplayed($data['displayed']);
$obj->setType($data['type']);
$obj->setProperties($data['properties']);
$obj->setActive($data['active']);
$obj->setMagnitude($data['magnitude']);
$obj->setValue($data['value']);
$obj->setNextEncodedFacets($data['nextEncodedFacets']);
return $obj;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
class MockProxy
{
protected static $mock;
/**
* Set static expectations
*
* @param mixed $mock
*/
public static function setStaticExpectations($mock)
{
static::$mock = $mock;
}
/**
* Any static calls we get are passed along to self::$mock. public static
*
* @param string $name
* @param mixed $args
*
* @return mixed
*/
public static function __callStatic($name, $args)
{
return call_user_func_array(
[static::$mock, $name],
$args
);
}
}
class StockAvailable extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Context extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Db extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Configuration extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Tools extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Category extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
public $id = null;
}
class Group extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Manufacturer extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Combination extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Shop extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Feature extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class FeatureValue extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}
class Module extends MockProxy
{
// Redeclare to use this instead MockProxy::mock
protected static $mock;
}

View File

@@ -0,0 +1,408 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Product;
use Configuration;
use Context;
use Db;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
use Ps_Facetedsearch;
use Smarty;
use Tools;
class SearchProviderTest extends MockeryTestCase
{
/**
* @var Search
*/
private $provider;
/**
* @var Db
*/
private $database;
/**
* @var Context
*/
private $context;
/**
* @var URLSerializer
*/
private $serializer;
/**
* @var FacetCollection
*/
private $facetCollection;
/**
* @var Ps_Facetedsearch
*/
private $module;
private function mockFacet($label, $data = ['filters' => []], $widgetType = 'checkbox')
{
$facet = Mockery::mock(Facet::class);
$facet->shouldReceive('getLabel')
->andReturn($label);
$facet->shouldReceive('toArray')
->andReturn($data);
$facet->shouldReceive('getWidgetType')
->andReturn($widgetType);
return $facet;
}
private function mockFilter($label, $active = false, $value = null, $properties = [])
{
$filter = Mockery::mock(Filter::class);
$filter->shouldReceive('getLabel')
->andReturn($label);
$filter->shouldReceive('isActive')
->andReturn($active);
if ($value !== null) {
$filter->shouldReceive('getValue')
->andReturn($value);
}
$filter->shouldReceive('getProperty')
->andReturnUsing(
function ($arg) use ($properties) {
return $properties[$arg];
}
);
return $filter;
}
protected function setUp()
{
$this->database = Mockery::mock(Db::class);
$this->context = Mockery::mock(Context::class);
$this->converter = Mockery::mock(Converter::class);
$this->serializer = Mockery::mock(URLSerializer::class);
$this->facetCollection = Mockery::mock(FacetCollection::class);
$this->module = Mockery::mock(Ps_Facetedsearch::class);
$this->module->shouldReceive('getDatabase')
->andReturn($this->database);
$this->module->shouldReceive('getContext')
->andReturn($this->context);
$this->module->shouldReceive('isAjax')
->andReturn(true);
$mock = Mockery::mock(Configuration::class);
$mock->shouldReceive('get')
->andReturnUsing(function ($arg) {
$valueMap = [
'PS_LAYERED_SHOW_QTIES' => true,
];
return $valueMap[$arg];
});
Configuration::setStaticExpectations($mock);
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getCurrentUrlProtocolPrefix')
->andReturn('http://');
Tools::setStaticExpectations($toolsMock);
$this->provider = new SearchProvider(
$this->module,
$this->converter,
$this->serializer
);
}
public function testRenderFacetsWithoutFacetsCollection()
{
$productContext = Mockery::mock(ProductSearchContext::class);
$productSearchResult = Mockery::mock(ProductSearchResult::class);
$productSearchResult->shouldReceive('getFacetCollection')
->once()
->andReturn(null);
$this->assertEquals(
'',
$this->provider->renderFacets(
$productContext,
$productSearchResult
)
);
}
public function testRenderFacetsWithFacetsCollection()
{
$productContext = Mockery::mock(ProductSearchContext::class);
$smarty = Mockery::mock(Smarty::class);
$smarty->shouldReceive('assign')
->once()
->with(
[
'show_quantities' => true,
'facets' => [
[
'filters' => [],
],
],
'js_enabled' => true,
'displayedFacets' => [],
'activeFilters' => [],
'sort_order' => 'product.position.asc',
'clear_all_link' => 'http://shop.prestashop.com/catalog?from=scratch',
]
);
$this->context->smarty = $smarty;
$sortOrder = Mockery::mock(SortOrder::class);
$sortOrder->shouldReceive('toString')
->once()
->andReturn('product.position.asc');
$productSearchResult = Mockery::mock(ProductSearchResult::class);
$productSearchResult->shouldReceive('getFacetCollection')
->once()
->andReturn($this->facetCollection);
$productSearchResult->shouldReceive('getCurrentSortOrder')
->once()
->andReturn($sortOrder);
$facet = $this->mockFacet('Test');
$this->facetCollection->shouldReceive('getFacets')
->once()
->andReturn(
[
$facet,
]
);
$this->module->shouldReceive('fetch')
->once()
->with(
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
)
->andReturn('');
$this->assertEquals(
'',
$this->provider->renderFacets(
$productContext,
$productSearchResult
)
);
}
public function testRenderFacetsWithFacetsCollectionAndFilters()
{
$productContext = Mockery::mock(ProductSearchContext::class);
$smarty = Mockery::mock(Smarty::class);
$smarty->shouldReceive('assign')
->once()
->with(
[
'show_quantities' => true,
'facets' => [
[
'displayed' => true,
'filters' => [
[
'label' => 'Men',
'type' => 'category',
'nextEncodedFacets' => 'Categories-Men',
'active' => false,
'facetLabel' => 'Test',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch&q=Categories-Men',
],
[
'label' => 'Women',
'type' => 'category',
'nextEncodedFacets' => '',
'active' => true,
'facetLabel' => 'Test',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch&page=1',
],
],
],
[
'displayed' => true,
'filters' => [
[
'label' => '£22.00 - £35.00',
'type' => 'price',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 0,
'nextEncodedFacets' => '',
'facetLabel' => 'Price',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch',
],
],
],
],
'js_enabled' => true,
'displayedFacets' => [
[
'displayed' => true,
'filters' => [
[
'label' => 'Men',
'type' => 'category',
'nextEncodedFacets' => 'Categories-Men',
'active' => false,
'facetLabel' => 'Test',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch&q=Categories-Men',
],
[
'label' => 'Women',
'type' => 'category',
'nextEncodedFacets' => '',
'active' => true,
'facetLabel' => 'Test',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch&page=1',
],
],
],
[
'displayed' => true,
'filters' => [
[
'label' => '£22.00 - £35.00',
'type' => 'price',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 0,
'nextEncodedFacets' => '',
'facetLabel' => 'Price',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch',
],
],
],
],
'activeFilters' => [
[
'label' => 'Women',
'type' => 'category',
'nextEncodedFacets' => '',
'active' => true,
'facetLabel' => 'Test',
'nextEncodedFacetsURL' => 'http://shop.prestashop.com/catalog?from=scratch&page=1',
],
],
'sort_order' => 'product.position.asc',
'clear_all_link' => 'http://shop.prestashop.com/catalog?from=scratch',
]
);
$this->context->smarty = $smarty;
$sortOrder = Mockery::mock(SortOrder::class);
$sortOrder->shouldReceive('toString')
->once()
->andReturn('product.position.asc');
$productSearchResult = Mockery::mock(ProductSearchResult::class);
$productSearchResult->shouldReceive('getFacetCollection')
->once()
->andReturn($this->facetCollection);
$productSearchResult->shouldReceive('getCurrentSortOrder')
->once()
->andReturn($sortOrder);
$facet = $this->mockFacet(
'Test',
[
'displayed' => true,
'filters' => [
[
'label' => 'Men',
'type' => 'category',
'nextEncodedFacets' => 'Categories-Men',
'active' => false,
],
[
'label' => 'Women',
'type' => 'category',
'nextEncodedFacets' => '',
'active' => true,
],
],
]
);
$facetSlider = $this->mockFacet(
'Price',
[
'displayed' => true,
'filters' => [
[
'label' => '£22.00 - £35.00',
'type' => 'price',
'active' => false,
'displayed' => true,
'properties' => [],
'magnitude' => 2,
'value' => 0,
'nextEncodedFacets' => '',
],
],
],
'slider'
);
$this->facetCollection->shouldReceive('getFacets')
->once()
->andReturn(
[
$facet,
$facetSlider,
]
);
$this->module->shouldReceive('fetch')
->once()
->with(
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
)
->andReturn('');
$this->assertEquals(
'',
$this->provider->renderFacets(
$productContext,
$productSearchResult
)
);
}
}

View File

@@ -0,0 +1,481 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Product;
use Configuration;
use Context;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL;
use PrestaShop\Module\FacetedSearch\Product\Search;
use stdClass;
use Tools;
class SearchTest extends MockeryTestCase
{
/**
* @var Search
*/
private $search;
protected function setUp()
{
$mock = Mockery::mock(Configuration::class);
$mock->shouldReceive('get')
->andReturnUsing(function ($arg) {
$valueMap = [
'PS_STOCK_MANAGEMENT' => true,
'PS_ORDER_OUT_OF_STOCK' => true,
'PS_HOME_CATEGORY' => true,
'PS_LAYERED_FULL_TREE' => false,
'PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY' => true,
];
return $valueMap[$arg];
});
Configuration::setStaticExpectations($mock);
$contextMock = Mockery::mock(Context::class);
$contextMock->shop = new stdClass();
$contextMock->shop->id = 1;
Context::setStaticExpectations($contextMock);
$this->search = new Search($contextMock);
}
public function testGetFacetedSearchTypeAdapter()
{
$this->assertInstanceOf(
MySQL::class,
$this->search->getSearchAdapter()
);
}
public function testInitSearchWithEmptyFilters()
{
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getValue')
->andReturnUsing(function ($arg) {
$valueMap = [
'id_category' => 12,
'id_category_layered' => 11,
];
return $valueMap[$arg];
});
Tools::setStaticExpectations($toolsMock);
$this->search->initSearch([]);
$this->assertEquals([], $this->search->getSearchAdapter()->getFilters()->toArray());
$this->assertEquals([], $this->search->getSearchAdapter()->getOperationsFilters()->toArray());
$this->assertEquals(
[
'id_category_default' => [
'=' => [
[
null,
],
],
],
'id_category' => [
'=' => [
[
null,
],
],
],
'id_shop' => [
'=' => [
[
1,
],
],
],
'visibility' => [
'=' => [
[
'both',
'catalog',
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals([], $this->search->getSearchAdapter()->getInitialPopulation()->getOperationsFilters()->toArray());
}
public function testInitSearchWithAllFilters()
{
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getValue')
->andReturnUsing(function ($arg) {
$valueMap = [
'id_category' => 12,
'id_category_layered' => 11,
];
return $valueMap[$arg];
});
Tools::setStaticExpectations($toolsMock);
$this->search->initSearch(
[
'id_feature' => [
[1, 2],
],
'id_attribute_group' => [
[4, 5],
],
'category' => [
[6],
],
'quantity' => [
0,
],
'weight' => [
'10',
'40',
],
'price' => [
'50',
'200',
],
'manufacturer' => [
'10',
],
'condition' => [
'1',
],
]
);
$this->assertEquals([], $this->search->getSearchAdapter()->getFilters()->toArray());
$this->assertEquals([], $this->search->getSearchAdapter()->getOperationsFilters()->toArray());
$this->assertEquals(
[
'weight' => [
'>=' => [
[
10.0,
],
],
'<=' => [
[
40.0,
],
],
],
'price_min' => [
'>=' => [
[
50.0,
],
],
],
'price_max' => [
'<=' => [
[
200.0,
],
],
],
'id_manufacturer' => [
'=' => [
[
'10',
],
],
],
'condition' => [
'=' => [
[
'1',
],
],
],
'id_shop' => [
'=' => [
[
1,
],
],
],
'visibility' => [
'=' => [
[
'both',
'catalog',
],
],
],
'id_category' => [
'=' => [
[
null,
],
[
6,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_stock_management' => [
[
[
'quantity',
[
0,
],
'<=',
],
[
'out_of_stock',
[
0,
],
'=',
],
],
],
'with_attributes_0' => [
[
[
'id_attribute',
[
4,
5,
],
],
],
],
'with_features_0' => [
[
[
'id_feature_value',
[
1,
2,
],
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getOperationsFilters()->toArray()
);
}
public function testInitSearchWithManyFeatures()
{
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getValue')
->andReturnUsing(function ($arg) {
$valueMap = [
'id_category' => 12,
'id_category_layered' => 11,
];
return $valueMap[$arg];
});
Tools::setStaticExpectations($toolsMock);
$this->search->initSearch(
[
'id_feature' => [
[1],
[2, 3, 4],
],
]
);
$this->assertEquals([], $this->search->getSearchAdapter()->getFilters()->toArray());
$this->assertEquals([], $this->search->getSearchAdapter()->getOperationsFilters()->toArray());
$this->assertEquals(
[
'id_shop' => [
'=' => [
[
1,
],
],
],
'visibility' => [
'=' => [
[
'both',
'catalog',
],
],
],
'id_category_default' => [
'=' => [
[
null,
],
],
],
'id_category' => [
'=' => [
[
null,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_features_0' => [
[
[
'id_feature_value',
[
1,
],
],
],
],
'with_features_1' => [
[
[
'id_feature_value',
[
2,
3,
4,
],
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getOperationsFilters()->toArray()
);
}
public function testInitSearchWithManyAttributes()
{
$toolsMock = Mockery::mock(Tools::class);
$toolsMock->shouldReceive('getValue')
->andReturnUsing(function ($arg) {
$valueMap = [
'id_category' => 12,
'id_category_layered' => 11,
];
return $valueMap[$arg];
});
Tools::setStaticExpectations($toolsMock);
$this->search->initSearch(
[
'id_attribute_group' => [
[1],
[2, 3, 4],
],
]
);
$this->assertEquals([], $this->search->getSearchAdapter()->getFilters()->toArray());
$this->assertEquals([], $this->search->getSearchAdapter()->getOperationsFilters()->toArray());
$this->assertEquals(
[
'id_shop' => [
'=' => [
[
1,
],
],
],
'visibility' => [
'=' => [
[
'both',
'catalog',
],
],
],
'id_category_default' => [
'=' => [
[
null,
],
],
],
'id_category' => [
'=' => [
[
null,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_attributes_0' => [
[
[
'id_attribute',
[
1,
],
],
],
],
'with_attributes_1' => [
[
[
'id_attribute',
[
2,
3,
4,
],
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getOperationsFilters()->toArray()
);
}
public function testAddFilter()
{
$this->search->addFilter('weight', [10, 20]);
$this->search->addFilter('id_feature', [[10, 20]]);
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Tests;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class URLSerializerTest extends MockeryTestCase
{
private $serializer;
protected function setUp()
{
$this->serializer = new URLSerializer();
}
private function mockFacet($label, $properties = [])
{
$facet = Mockery::mock(Facet::class);
$facet->shouldReceive('getLabel')
->andReturn($label);
$facet->shouldReceive('getProperty')
->andReturnUsing(
function ($arg) use ($properties) {
return $properties[$arg];
}
);
return $facet;
}
private function mockFilter($label, $active = false, $value = null, $properties = [])
{
$filter = Mockery::mock(Filter::class);
$filter->shouldReceive('getLabel')
->andReturn($label);
$filter->shouldReceive('isActive')
->andReturn($active);
if ($value !== null) {
$filter->shouldReceive('getValue')
->andReturn($value);
}
$filter->shouldReceive('getProperty')
->andReturnUsing(
function ($arg) use ($properties) {
return $properties[$arg];
}
);
return $filter;
}
public function testGetActiveFilters()
{
$first = $this->mockFilter('Tops', true);
$second = $this->mockFilter('Robes', false);
$facet = $this->mockFacet('Categories', ['range' => false]);
$facet->shouldReceive('getFilters')
->andReturn([$first, $second]);
$this->assertEquals(
['Categories' => ['Tops' => 'Tops']],
$this->serializer->getActiveFacetFiltersFromFacets([$facet])
);
}
public function testGetActiveFiltersWithRange()
{
$filter = $this->mockFilter('filter', true, [0, 100], ['symbol' => '$']);
$facet = $this->mockFacet('Price', ['range' => true]);
$facet->shouldReceive('getFilters')
->andReturn([$filter]);
$this->assertEquals(
['Price' => ['$', 0, 100]],
$this->serializer->getActiveFacetFiltersFromFacets([$facet])
);
}
public function testAddAndRemoveFiltersWithoutRange()
{
$filter = $this->mockFilter('Tops');
$facet = $this->mockFacet('Categories', ['range' => false]);
$facetsFilters = $this->serializer->addFilterToFacetFilters(
[],
$filter,
$facet
);
$this->assertEquals(
['Categories' => ['Tops' => 'Tops']],
$facetsFilters
);
$facetsFilters = $this->serializer->removeFilterFromFacetFilters(
$facetsFilters,
$filter,
$facet
);
$this->assertEquals(
[],
$facetsFilters
);
}
public function testAddAndRemoveFiltersWithRangeAndMinMax()
{
$filter = $this->mockFilter(
'filter',
true,
[0, 100],
['symbol' => '$']
);
$facet = $this->mockFacet(
'Price',
[
'range' => true,
'values' => [],
'min' => 0,
'max' => 200,
]
);
$facetsFilters = $this->serializer->addFilterToFacetFilters(
[],
$filter,
$facet
);
$this->assertEquals(
['Price' => ['$', 0, 200]],
$facetsFilters
);
$facetsFilters = $this->serializer->removeFilterFromFacetFilters(
$facetsFilters,
$filter,
$facet
);
$this->assertEquals(
[],
$facetsFilters
);
}
public function testAddAndRemoveFiltersWithRange()
{
$filter = $this->mockFilter(
'filter',
true,
[0, 100],
['symbol' => '$']
);
$facet = $this->mockFacet(
'Price',
[
'range' => true,
'values' => [10, 100],
]
);
$facetsFilters = $this->serializer->addFilterToFacetFilters(
[],
$filter,
$facet
);
$this->assertEquals(
['Price' => ['$', 10, 100]],
$facetsFilters
);
$facetsFilters = $this->serializer->removeFilterFromFacetFilters(
$facetsFilters,
$filter,
$facet
);
$this->assertEquals(
[],
$facetsFilters
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
// FacetedSearch autoloader
require __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/FacetedSearch/MockProxy.php';
require_once __DIR__ . '/FacetedSearch/Mock/Filter.php';
require_once __DIR__ . '/FacetedSearch/Mock/Facet.php';
require_once __DIR__ . '/FacetedSearch/Interface/WidgetInterface.php';
require_once __DIR__ . '/FacetedSearch/Interface/FacetsRendererInterface.php';
require_once __DIR__ . '/FacetedSearch/Interface/ProductSearchProviderInterface.php';
define('_PS_COL_IMG_DIR_', __DIR__ . '/files/');
// Fake pSQL function
function pSQL($string, $htmlOK = false)
{
return $string;
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
$rootDir = getenv('_PS_ROOT_DIR_');
if (!$rootDir) {
echo '[ERROR] Define _PS_ROOT_DIR_ with the path to PrestaShop folder' . PHP_EOL;
exit(1);
}
// Add module composer autoloader
require_once dirname(__DIR__) . '/../../vendor/autoload.php';
// Add PrestaShop composer autoload
define('_PS_ADMIN_DIR_', $rootDir . '/admin-dev/');
define('PS_ADMIN_DIR', _PS_ADMIN_DIR_);
require_once $rootDir . '/config/defines.inc.php';
require_once $rootDir . '/config/autoload.php';
require_once $rootDir . '/config/bootstrap.php';
// Make sure loader php-parser is coming from php stan composer
$loader = new \Composer\Autoload\ClassLoader();
$loader->setPsr4('PhpParser\\', ['/composer/vendor/nikic/php-parser/lib/PhpParser']);
$loader->register(true);
// We must declare these constant in this boostrap script.
// Ignoring the error partern with this value will throw another error if not found
// during the checks.
$constantsToDefine = [
'_DB_PREFIX_',
'_PS_SSL_PORT_',
'_THEME_NAME_',
'_PARENT_THEME_NAME_',
'__PS_BASE_URI__',
'_PS_PRICE_DISPLAY_PRECISION_',
'_PS_PRICE_COMPUTE_PRECISION_',
'_PS_OS_CHEQUE_',
'_PS_OS_PAYMENT_',
'_PS_OS_PREPARATION_',
'_PS_OS_SHIPPING_',
'_PS_OS_DELIVERED_',
'_PS_OS_CANCELED_',
'_PS_OS_REFUND_',
'_PS_OS_ERROR_',
'_PS_OS_OUTOFSTOCK_',
'_PS_OS_OUTOFSTOCK_PAID_',
'_PS_OS_OUTOFSTOCK_UNPAID_',
'_PS_OS_BANKWIRE_',
'_PS_OS_PAYPAL_',
'_PS_OS_WS_PAYMENT_',
'_PS_OS_COD_VALIDATION_',
];
foreach ($constantsToDefine as $constant) {
if (!defined($constant)) {
define($constant, 'DUMMY_VALUE');
}
}

View File

@@ -0,0 +1,18 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~constant NUMBERING_SYSTEM_LATIN on an unknown class~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
- '~Call to an undefined method PrestaShop\\PrestaShop\\Adapter\\Tools::linkRewrite\(\).~'
level: 5

View File

@@ -0,0 +1,18 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~constant NUMBERING_SYSTEM_LATIN on an unknown class~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
- '~Call to an undefined method PrestaShop\\PrestaShop\\Adapter\\Tools::linkRewrite\(\).~'
level: 5

View File

@@ -0,0 +1,17 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~constant NUMBERING_SYSTEM_LATIN on an unknown class~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
level: 5

View File

@@ -0,0 +1,16 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
level: 5

View File

@@ -0,0 +1,16 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
level: 5

View File

@@ -0,0 +1,17 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /var/www/html/modules/ps_facetedsearch/tests/php/phpstan/bootstrap.php
paths:
- /var/www/html/modules/ps_facetedsearch/src
ignoreErrors:
# module specific
- '~Constant _THEME_COL_DIR_ not found.~'
- '~Iterating over an object of an unknown class mysqli_result\.~'
- '~Access to offset mixed on an unknown class mysqli_result\.~'
- '~Parameter #1 \$master of static method DbCore::getInstance\(\) expects bool, int given\.~'
- '~Parameter #1 \$string of method PrestaShop\\PrestaShop\\Core\\Product\\Search\\URLFragmentSerializer::unserialize\(\) expects string, array given\.~'
- '~Parameter #\d+ \$(.+?) of class Category constructor expects null, int given\.~'
- '~constant NUMBERING_SYSTEM_LATIN on an unknown class~'
- '~PrestaShopBundle\\Form\\Admin\\Type\\(TranslatableType|SwitchType) not found~'
level: 5

View File

@@ -0,0 +1,23 @@
<phpunit
bootstrap="bootstrap.php"
>
<php>
<const name="_DB_PREFIX_" value="ps_"/>
<const name="_PS_VERSION_" value="FacetedSearchVersion"/>
<const name="_THEME_COL_DIR_" value="/theme/"/>
<server name="REQUEST_URI" value="/catalog?from=scratch&amp;page=1&amp;something"/>
<server name="HTTP_HOST" value="shop.prestashop.com"/>
</php>
<testsuites>
<testsuite name="FacetedSearch">
<directory>.</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../../src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,64 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @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_0_0(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'categoryAddition',
'categoryUpdate',
'attributeGroupForm',
'afterSaveAttributeGroup',
'afterDeleteAttributeGroup',
'featureForm',
'afterDeleteFeature',
'afterSaveFeature',
'categoryDeletion',
'afterSaveProduct',
'postProcessAttributeGroup',
'postProcessFeature',
'featureValueForm',
'postProcessFeatureValue',
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'attributeForm',
'postProcessAttribute',
'afterDeleteAttribute',
'afterSaveAttribute',
'productSearchProvider',
'displayLeftColumn',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildLayeredStructure();
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @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_0_3(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'postProcessFeatureValue',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
$newHooks = [
'actionFeatureSave',
'actionFeatureValueDelete',
'displayFeatureValuePostProcess',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @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_3_0(Ps_Facetedsearch $module)
{
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

Some files were not shown because too many files have changed in this diff Show More