Initial commit

This commit is contained in:
2019-11-20 07:44:43 +01:00
commit 5bf49c4a81
41188 changed files with 5459177 additions and 0 deletions

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,68 @@
# Faceted search module
## About
Displays a block with layered navigation filters.
## 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.
```
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 solution. Everyone is welcome and even encouraged to contribute with their own improvements.
### Requirements
Contributors **must** follow the following rules:
* **Make your Pull Request on the "dev" branch**, NOT the "master" branch.
* Do not update the module's version number.
* Follow [the coding standards][1].
### Process in details
Contributors wishing to edit a module's files should follow the following process:
1. Create your GitHub account, if you do not have one already.
2. Fork this project to your GitHub account.
3. Clone your fork to your local machine in the ```/modules``` directory of your PrestaShop installation.
4. Create a branch in your local clone of the module for your changes.
5. Change the files in your branch. Be sure to follow the [coding standards][1]!
6. Push your changed branch to your fork in your GitHub account.
7. Create a pull request for your changes **on the _'dev'_ branch** of the module's project. Be sure to follow the [contribution guidelines][2] in your pull request. If you need help to make a pull request, read the [GitHub help page about creating pull requests][3].
8. Wait for one of the core developers either to include your change in the codebase, or to comment on possible improvements you should make to your code.
That's it: you have contributed to this open source project! Congratulations!
## 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
[1]: https://devdocs.prestashop.com/1.7/development/coding-standards/
[2]: https://devdocs.prestashop.com/1.7/contribute/contribution-guidelines/
[3]: https://help.github.com/articles/using-pull-requests
[AFL-3.0]: https://opensource.org/licenses/AFL-3.0

View File

@@ -0,0 +1,56 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
.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,226 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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;
});
$('.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;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations.price_indexation_finished);
$('#ajax-message-ok').show();
return;
}
this.cursor = parseInt(res.cursor, 10);
$(this).html(this.legend + translations.price_indexation_in_progress.replace('%s', res.count));
$(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');
});
}
});

View File

@@ -0,0 +1,32 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
class LocalizationException {
constructor(message) {
this.message = message;
this.name = 'LocalizationException';
}
}
export default LocalizationException;

View File

@@ -0,0 +1,35 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,297 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
/**
* 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 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(majorDigits < 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();
let num = number;
num = num.split(DECIMAL_SEPARATOR_PLACEHOLDER).join(symbols.getDecimal());
num = num.split(GROUP_SEPARATOR_PLACEHOLDER).join(symbols.getGroup());
num = num.split(MINUS_SIGN_PLACEHOLDER).join(symbols.getMinusSign());
num = num.split(PERCENT_SYMBOL_PLACEHOLDER).join(symbols.getPercentSign());
num = num.split(PLUS_SIGN_PLACEHOLDER).join(symbols.getPlusSign());
return num;
}
/**
* 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) {
const 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,228 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,176 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,115 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,39 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,55 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
@mixin text-ellipsis() {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#search_filters {
.facet {
.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,28 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
import 'jquery-ui-touch-punch';
import './events';
import './slider.scss';
import './facet.scss';

View File

@@ -0,0 +1,49 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,66 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
.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,134 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,46 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
#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,34 @@
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,34 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,46 @@
{
"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"
]
}
}

2757
web/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.0.5]]></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,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Navigation &agrave; facettes]]></displayName>
<version><![CDATA[3.0.5]]></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>

View File

@@ -0,0 +1,29 @@
.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;
}
.bootstrap .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);
}
.bootstrap .filter_panel header {
margin-bottom: 7px;
}

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,34 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,34 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,34 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,35 @@
<?php
/*
* 2007-2015 PrestaShop
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/afl-3.0.php
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,85 @@
/*
* HTML5 Sortable jQuery Plugin
* http://farhadi.ir/projects/html5sortable
*
* Copyright 2012, Ali Farhadi
* Released under the MIT license.
*/
(function($) {
var dragging, placeholders = $();
$.fn.sortable = function(options) {
var method = String(options);
options = $.extend({
connectWith: false
}, options);
return this.each(function() {
if (/^enable|disable|destroy$/.test(method)) {
var items = $(this).children($(this).data('items')).attr('draggable', method == 'enable');
if (method == 'destroy') {
items.add(this).removeData('connectWith items')
.off('dragstart.h5s dragend.h5s selectstart.h5s dragover.h5s dragenter.h5s drop.h5s');
}
return;
}
var isHandle, index, items = $(this).children(options.items);
var placeholder = $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="sortable-placeholder">');
items.find(options.handle).mousedown(function() {
isHandle = true;
}).mouseup(function() {
isHandle = false;
});
$(this).data('items', options.items)
placeholders = placeholders.add(placeholder);
if (options.connectWith) {
$(options.connectWith).add(this).data('connectWith', options.connectWith);
}
items.attr('draggable', 'true').on('dragstart.h5s', function(e) {
if (options.handle && !isHandle) {
return false;
}
isHandle = false;
var dt = e.originalEvent.dataTransfer;
dt.effectAllowed = 'move';
dt.setData('Text', 'dummy');
index = (dragging = $(this)).addClass('sortable-dragging').index();
}).on('dragend.h5s', function() {
if (!dragging) {
return;
}
dragging.removeClass('sortable-dragging').show();
placeholders.detach();
if (index != dragging.index()) {
dragging.parent().trigger('sortupdate', {item: dragging, start_index: index, end_index: dragging.index()});
}
dragging = null;
}).not('a[href], img').on('selectstart.h5s', function() {
this.dragDrop && this.dragDrop();
return false;
}).end().add([this, placeholder]).on('dragover.h5s dragenter.h5s drop.h5s', function(e) {
if (!items.is(dragging) && options.connectWith !== $(dragging).parent().data('connectWith')) {
return true;
}
if (e.type == 'drop') {
e.stopPropagation();
placeholders.filter(':visible').after(dragging);
dragging.trigger('dragend.h5s');
return false;
}
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = 'move';
if (items.is(this)) {
if (options.forcePlaceholderSize) {
placeholder.height(dragging.outerHeight());
}
dragging.hide();
$(this)[placeholder.index() < $(this).index() ? 'after' : 'before'](placeholder);
placeholders.not(placeholder).detach();
} else if (!placeholders.is(this) && !$(this).children(options.items).length) {
placeholders.detach();
$(this).append(placeholder);
}
return false;
});
});
};
})(jQuery);

View File

@@ -0,0 +1,219 @@
function checkForm()
{
var is_category_selected = false;
var is_filter_selected = false;
$('#categories-treeview input[type=checkbox]').each(
function()
{
if ($(this).prop('checked'))
{
is_category_selected = true;
return false;
}
}
);
$('.filter_list_item input[type=checkbox]').each(
function()
{
if ($(this).prop('checked'))
{
is_filter_selected = true;
return false;
}
}
);
if (!is_category_selected)
{
alert(translations['no_selected_categories']);
$('#categories-treeview input[type=checkbox]').first().focus();
return false;
}
if (!is_filter_selected)
{
alert(translations['no_selected_filters']);
$('#filter_list_item input[type=checkbox]').first().focus();
return false;
}
return true;
}
$(document).ready(
function()
{
$('.ajaxcall').click(
function()
{
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;
var type = $(this).attr('rel');
$.ajax(
{
url: this.href+'&ajax=1',
context: this,
dataType: 'json',
cache: 'false',
success: function(res)
{
this.running = false;
this.restartAllowed = true;
$('#indexing-warning').hide();
$(this).html(this.legend);
if (type == 'price')
$('#ajax-message-ok span').html(translations['url_indexation_finished']);
else
$('#ajax-message-ok span').html(translations['attribute_indexation_finished']);
$('#ajax-message-ok').show();
return;
},
error: function(res)
{
this.restartAllowed = true;
$('#indexing-warning').hide();
if (type == 'price')
$('#ajax-message-ko span').html(translations['url_indexation_failed']);
else
$('#ajax-message-ko span').html(translations['attribute_indexation_failed']);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.running = false;
}
}
);
return false;
});
$('.ajaxcall-recurcive').each(
function(it, elm)
{
$(elm).click(
function()
{
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: function(res)
{
this.running = false;
if (res.result)
{
this.cursor = 0;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations['price_indexation_finished']);
$('#ajax-message-ok').show();
return;
}
this.cursor = parseInt(res.cursor);
$(this).html(this.legend+translations['price_indexation_in_progress'].replace('%s', res.count));
$(this).click();
},
error: function(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()
{
var current_selected_filters_count = parseInt($('#selected_filters').html());
if ($(this).prop('checked'))
$('#selected_filters').html(current_selected_filters_count+1);
else
$('#selected_filters').html(current_selected_filters_count-1);
}
);
if (typeof filters !== 'undefined')
{
filters = JSON.parse(filters);
for (filter in filters)
{
$('#'+filter).attr("checked","checked");
$('#selected_filters').html(parseInt($('#selected_filters').html())+1);
$('select[name="'+filter+'_filter_type"]').val(filters[filter].filter_type);
$('select[name="'+filter+'_filter_show_limit"]').val(filters[filter].filter_show_limit);
}
}
}
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
{
"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": "",
"license": "ISC",
"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.2.3",
"@babel/core": "^7.3.4",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.4.1",
"@babel/register": "^7.4.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"clean-webpack-plugin": "^2.0.0",
"css-loader": "^2.1.0",
"eslint": "^5.15.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prestashop": "0.0.2",
"eslint-plugin-import": "^2.16.0",
"mini-css-extract-plugin": "^0.5.0",
"mocha": "^6.1.4",
"node-sass": "^4.11.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
},
"dependencies": {
"jquery-ui-touch-punch": "^0.2.3"
}
}

View File

@@ -0,0 +1,17 @@
<?php
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,11 @@
<?php
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
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');
}
if (!Tools::getValue('ajax')) {
// Case of nothing to do but showing a message (1)
if (Tools::getValue('return_message') !== false) {
echo '1';
die();
}
if (Tools::usingSecureMode()) {
$domain = Tools::getShopDomainSsl(true);
} else {
$domain = Tools::getShopDomain(true);
}
// Return a content without waiting the end of index execution
header('Location: ' . $domain . __PS_BASE_URI__ . 'modules/ps_facetedsearch/ps_facetedsearch-price-indexer.php?token=' . Tools::getValue('token') . '&return_message=' . (int) Tools::getValue('cursor'));
flush();
}
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,7 @@
{if isset($rendered_active_filters)}
{$rendered_active_filters nofilter}
{/if}
{if isset($rendered_facets)}
{$rendered_facets nofilter}
{/if}

View File

@@ -0,0 +1,330 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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;
}
/**
* {@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 = $adapter->getFilters();
}
/**
* {@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,300 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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);
/**
* 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,698 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Db;
use Context;
use StockAvailable;
use Doctrine\Common\Collections\ArrayCollection;
class MySQL extends AbstractAdapter
{
/**
* @var string
*/
const TYPE = 'MySQL';
/**
* @var string
*/
const LEFT_JOIN = 'LEFT JOIN';
/**
* @var string
*/
const INNER_JOIN = 'INNER JOIN';
/**
* @var string
*/
const STRAIGHT_JOIN = 'STRAIGHT_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() . ')';
}
if (empty($this->getSelectFields())
&& empty($this->getFilters())
&& empty($this->getGroupFields())
&& empty($this->getOrderField())
) {
// avoid adding an extra SELECT FROM (SELECT ...) if it's not needed
$query = $referenceTable;
$this->setOrderField('');
} else {
$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::STRAIGHT_JOIN,
],
'id_attribute' => [
'tableName' => 'product_attribute_combination',
'tableAlias' => 'pac',
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
'joinType' => self::STRAIGHT_JOIN,
'dependencyField' => 'id_product_attribute',
],
'id_attribute_group' => [
'tableName' => 'attribute',
'tableAlias' => 'a',
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
'joinType' => self::STRAIGHT_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 0 = sa.id_product_attribute ' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
],
'quantity' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND 0 = sa.id_product_attribute ' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
],
'price_min' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product 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_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_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_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',
],
];
return $filterToTableMapping;
}
/**
* Compute the orderby fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return string
*/
private 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';
}
if (array_key_exists($orderField, $filterToTableMapping)
&& (
// If the requested order field is in the result, no need to change tableAlias
// unless a fieldName key exists
isset($filterToTableMapping[$orderField]['fieldName'])
|| $this->getInitialPopulation() === null
|| !$this->getInitialPopulation()->getSelectFields()->contains($orderField)
)
) {
$joinMapping = $filterToTableMapping[$orderField];
$orderField = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $orderField);
} else {
$orderField = 'p.' . $orderField;
}
return $orderField;
}
/**
* Compute the select fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeSelectFields(array $filterToTableMapping)
{
$selectFields = [];
foreach ($this->getSelectFields() as $key => $selectField) {
$selectAlias = 'p';
if (array_key_exists($selectField, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$selectField];
$selectAlias = $joinMapping['tableAlias'];
if (isset($joinMapping['fieldName'])) {
$selectField = $joinMapping['fieldName'];
}
}
if (strpos($selectField, '(') !== false) {
$selectFields[] = $selectField;
} else {
$selectFields[] = $selectAlias . '.' . $selectField;
}
}
return $selectFields;
}
/**
* Computer the where conditions that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeWhereConditions(array $filterToTableMapping)
{
$whereConditions = [];
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'] . ($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) . ')';
}
$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
*/
private function computeJoinConditions(array $filterToTableMapping)
{
$joinList = new ArrayCollection();
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
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) {
// Index is not the first, append index to tableAlias on joinCondition
$joinMapping['joinCondition'] = preg_replace(
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
'${1}' . $idx . '.',
$joinMapping['joinCondition']
);
$joinMapping['tableAlias'] .= $idx;
}
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
}
$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 (empty($this->getGroupFields())) {
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);
$mysqlAdapter->setSelectFields(['COUNT(DISTINCT p.id_product) c']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return isset($result[0]['c']) ? $result[0]['c'] : 0;
}
/**
* {@inheritdoc}
*/
public function valueCount($fieldName)
{
$this->resetGroupBy();
$this->addGroupBy($fieldName);
$this->addSelectField($fieldName);
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
$this->setLimit(null);
$this->setOrderField('');
return $this->execute();
}
/**
* {@inheritdoc}
*/
public function useFiltersAsInitialPopulation()
{
$this->setLimit(null);
$this->setOrderField('');
$this->setSelectFields(
[
'id_product',
'id_manufacturer',
'quantity',
'condition',
'weight',
'price',
]
);
$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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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;
/**
* @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'])
->setMultipleSelectionAllowed(true);
switch ($filterBlock['type']) {
case 'category':
case 'quantity':
case 'condition':
case 'manufacturer':
case 'id_attribute_group':
case 'id_feature':
$type = $filterBlock['type'];
if ($filterBlock['type'] == 'quantity') {
$type = 'availability';
} elseif ($filterBlock['type'] == 'id_attribute_group') {
$type = 'attribute_group';
$facet->setProperty('id_attribute_group', $filterBlock['id_key']);
} elseif ($filterBlock['type'] == 'id_feature') {
$type = 'feature';
$facet->setProperty('id_feature', $filterBlock['id_key']);
}
$facet->setType($type);
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']) && $filterArray['color'] != '') {
$filter->setProperty('color', $filterArray['color']);
}
if (isset($filterArray['url_name']) && $filterArray['url_name'] != '') {
$filter->setProperty('texture', _THEME_COL_DIR_ . $id . '.jpg');
}
$facet->addFilter($filter);
}
break;
case 'weight':
case '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 '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 '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 '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 'id_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 'id_attribute_group':
$attributesGroup = AttributeGroup::getAttributesGroups($idLang);
foreach ($attributesGroup as $attributeGroup) {
if ($filter['id_value'] == $attributeGroup['id_attribute_group']
&& isset($facetAndFiltersLabels[$attributeGroup['name']])
) {
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['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 'price':
case '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 '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 'price':
case 'weight':
if ($value[0] === '' && $value[1] === '') {
unset($searchFilters[$key]);
}
break;
default:
if ($value == '' || $value == []) {
unset($searchFilters[$key]);
}
break;
}
}
return $searchFilters;
}
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case 'price':
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case 'weight':
return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop');
case 'condition':
return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop');
case 'quantity':
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
case 'manufacturer':
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
case 'category':
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
case 'id_feature':
case 'id_attribute_group':
default:
return null;
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\Product\Search;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use Product;
use Validate;
use Configuration;
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,66 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Db;
use Context;
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,131 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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,156 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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,103 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,46 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,152 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Tools;
use Language;
class Feature extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
];
/**
* 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)
{
$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;
}
if ($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']
)) {
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;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
(`id_feature`, `indexable`)
VALUES (' . (int) $params['id_feature'] . ', ' . (int) Tools::getValue('layered_indexable') . ')'
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
(`id_feature`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_feature'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL(Tools::getValue('meta_title_' . (int) $language['id_lang']), true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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,50 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,78 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
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,117 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,
];
/**
* 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,288 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use Configuration;
use Tools;
use Category;
use Context;
class Search
{
/**
* @var bool
*/
private $psStockManagement;
/**
* @var bool
*/
private $psOrderOutOfStock;
/**
* @var AbstractAdapter
*/
private $searchAdapter;
/**
* @var Context
*/
private $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_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 $filterValue) {
$operationsFilter[] = ['id_feature_value', $filterValue];
}
$this->getSearchAdapter()->addOperationsFilter(
'with_features',
[$operationsFilter]
);
break;
case 'id_attribute_group':
$operationsFilter = [];
foreach ($filterValues as $filterValue) {
$operationsFilter[] = ['id_attribute', $filterValue];
}
$this->getSearchAdapter()->addOperationsFilter(
'with_attributes',
[$operationsFilter]
);
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(
'with_stock_management',
$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,505 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use Context;
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;
public function __construct(
Ps_Facetedsearch $module,
Filters\Converter $converter,
URLSerializer $serializer
) {
$this->module = $module;
$this->filtersConverter = $converter;
$this->facetsSerializer = $serializer;
}
/**
* @return array
*/
private function getAvailableSortOrders()
{
$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 [
$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 = new Search($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->hideZeroValues($facets);
$this->hideUselessFacets($facets);
$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 '';
}
return $this->module->render(
'views/templates/front/catalog/facets.tpl',
[
'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,
]
),
]
);
}
/**
* 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);
return $this->module->render(
'views/templates/front/catalog/active-filters.tpl',
[
'activeFilters' => $activeFilters,
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
}
/**
* 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']) {
$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 ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
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];
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($min),
$unit,
Tools::displayNumber($max),
$unit
)
);
}
} elseif ($facet->getType() === 'price') {
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];
$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)
);
}
}
}
/**
* Hide entries with 0 results
*
* @param array $facets
*/
private function hideZeroValues(array $facets)
{
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() === 0) {
$filter->setDisplayed(false);
}
}
}
}
/**
* Remove the facet when there's only 1 result.
* Keep facet status when it's a slider
*
* @param array $facets
*/
private function hideUselessFacets(array $facets)
{
foreach ($facets as $facet) {
if ($facet->getWidgetType() === 'slider') {
$facet->setDisplayed(
$facet->getProperty('min') != $facet->getProperty('max')
);
continue;
}
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0) {
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
$usefulFiltersCount > 1
);
}
}
/**
* 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,110 @@
<?php
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class Ps_FacetedsearchFacetsURLSerializer
{
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, $facet) {
if ($facet->getProperty('range')) {
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetFilter->getValue()['from'],
$facetFilter->getValue()['to'],
];
} else {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
return $facetFilters;
}
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;
}
public function getActiveFacetFiltersFromFacets(array $facets) {
$facetFilters = [];
foreach ($facets as $facet) {
if ($facet->getProperty('range')) {
foreach ($facet->getFilters() as $facetFilter) {
if ($facetFilter->isActive()) {
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetFilter->getValue()['from'],
$facetFilter->getValue()['to'],
];
}
}
} else {
foreach ($facet->getFilters() as $facetFilter) {
if ($facetFilter->isActive()) {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
}
}
}
return $facetFilters;
}
public function serialize(array $facets)
{
$facetFilters = $this->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
return $urlSerializer->serialize($facetFilters);
}
public function setFiltersFromEncodedFacets(array $facets, $encodedFacets)
{
$urlSerializer = new URLFragmentSerializer();
$facetAndFiltersLabels = $urlSerializer->unserialize($encodedFacets);
foreach ($facetAndFiltersLabels as $facetLabel => $filters) {
foreach ($facets as $facet) {
if ($facet->getLabel() === $facetLabel) {
if (true === $facet->getProperty('range')) {
$symbol = $filters[0];
$from = $filters[1];
$to = $filters[2];
$found = false;
foreach ($facet->getFilters() as $filter) {
if ($from >= $filter->getValue()['from'] && $to <= $filter->getValue()['to']) {
$filter->setActive(true);
$found = true;
}
}
if (!$found) {
$filter = new Filter();
$filter->setValue([
'from' => $from,
'to' => $to,
])->setProperty('symbol', $symbol);
$filter->setActive(true);
$facet->addFilter($filter);
}
} else {
foreach ($filters as $filterLabel) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getLabel() === $filterLabel) {
$filter->setActive(true);
}
}
}
}
}
}
}
return $facets;
}
}

View File

@@ -0,0 +1,169 @@
<?php
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class Ps_FacetedsearchFiltersConverter
{
public function getFacetsFromFacetedSearchFilters(array $facetedSearchFilters)
{
$facets = [];
foreach ($facetedSearchFilters as $facetArray) {
$facet = new Facet();
$facet
->setLabel($facetArray['name'])
->setMultipleSelectionAllowed(true)
;
switch ($facetArray['type']) {
case 'category':
case 'quantity':
case 'condition':
case 'manufacturer':
case 'id_attribute_group':
case 'id_feature':
$type = $facetArray['type'];
if ($facetArray['type'] == 'quantity') {
$type = 'availability';
} elseif ($facetArray['type'] == 'id_attribute_group') {
$type = 'attribute_group';
$facet->setProperty('id_attribute_group', $facetArray['id_key']);
} elseif ($facetArray['type'] == 'id_feature') {
$type = 'feature';
$facet->setProperty('id_feature', $facetArray['id_key']);
}
$facet->setType($type);
foreach ($facetArray['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id)
;
if (isset($facetArray['is_color_group']) && $facetArray['is_color_group']){
if (isset($filterArray['color']) && $filterArray['color'] != '') {
$filter->setProperty('color', $filterArray['color']);
}
if (isset($filterArray['url_name']) && $filterArray['url_name'] != '') {
$filter->setProperty('texture', _THEME_COL_DIR_.$id.'.jpg');
}
}
$facet->addFilter($filter);
}
break;
case 'weight':
case 'price':
$facet
->setType($facetArray['type'])
->setProperty('min', $facetArray['min'])
->setProperty('max', $facetArray['max'])
->setMultipleSelectionAllowed(false)
->setProperty('range', true)
;
foreach ($facetArray['list_of_values'] as $value) {
$filter = new Filter();
$filter
->setType($facetArray['type'])
->setMagnitude($value['nbr'])
->setProperty('symbol', $facetArray['unit'])
->setValue([
'from' => $value[0],
'to' => $value[1],
])
;
$facet->addFilter($filter);
}
break;
}
switch ((int) $facetArray['filter_type']) {
case 0: // checkbox
$facet->setMultipleSelectionAllowed(true);
$facet->setWidgetType('checkboxes');
break;
case 1: // radio
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('radio-buttons');
break;
case 2: // drop down
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('dropdown');
break;
}
$facets[] = $facet;
}
return $facets;
}
/**
* WARNING, this is not the inverse function of
* getFacetsFromFacetedSearchFilters
* because facetedsearch doesn't use the same representation
* of filters in input as in output.
* It is close to the inverse function for our use though, hence the name.
*/
public function getFacetedSearchFiltersFromFacets(array $facets)
{
$facetedSearchFilters = [];
foreach ($facets as $facet) {
switch ($facet->getType()) {
case 'category':
case 'availability':
case 'condition':
case 'manufacturer':
case 'attribute_group':
case 'feature':
$type = $facet->getType();
if ($type === 'availability') {
$type = 'quantity';
} elseif ($type === 'attribute_group') {
$type = 'id_attribute_group';
} elseif ($type === 'feature') {
$type = 'id_feature';
}
if (!isset($facetedSearchFilters[$type])) {
$facetedSearchFilters[$type] = [];
}
foreach ($facet->getFilters() as $filter) {
if (!$filter->isActive()) {
continue;
}
$key = count($facetedSearchFilters[$type]);
$value = $filter->getValue();
if ($type === 'id_attribute_group') {
$key = $value;
$value = $facet->getProperty('id_attribute_group').'_'.$filter->getValue();
}
if ($type === 'id_feature') {
$key = $value;
$value = $facet->getProperty('id_feature').'_'.$filter->getValue();
}
$facetedSearchFilters[$type][$key] = $value;
}
break;
case 'weight':
case 'price':
foreach ($facet->getFilters() as $filter) {
if (!$filter->isActive()) {
continue;
}
$facetedSearchFilters[$facet->getType()] = [
$filter->getValue()['from'],
$filter->getValue()['to'],
];
break;
}
break;
}
}
return $facetedSearchFilters;
}
}

View File

@@ -0,0 +1,292 @@
<?php
require_once __DIR__.DIRECTORY_SEPARATOR.'Ps_FacetedsearchFiltersConverter.php';
require_once __DIR__.DIRECTORY_SEPARATOR.'Ps_FacetedsearchFacetsURLSerializer.php';
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
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\SortOrder;
class Ps_FacetedsearchProductSearchProvider implements ProductSearchProviderInterface
{
private $module;
private $filtersConverter;
private $facetsSerializer;
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
$this->filtersConverter = new Ps_FacetedsearchFiltersConverter();
$this->facetsSerializer = new Ps_FacetedsearchFacetsURLSerializer();
}
public function getFacetCollectionFromEncodedFacets(
ProductSearchQuery $query
) {
// do not compute range filters, all info we need is encoded in $encodedFacets
$compute_range_filters = false;
$filterBlock = $this->module->getFilterBlock(
[],
$compute_range_filters
);
$queryTemplate = $this->filtersConverter->getFacetsFromFacetedSearchFilters(
$filterBlock['filters']
);
$facets = $this->facetsSerializer->setFiltersFromEncodedFacets(
$queryTemplate,
$query->getEncodedFacets()
);
return (new FacetCollection())->setFacets($facets);
}
private function copyFiltersActiveState(
array $sourceFacets,
array $targetFacets
) {
$copyByLabel = function (Facet $source, Facet $target) {
foreach ($target->getFilters() as $targetFilter) {
foreach ($source->getFilters() as $sourceFilter) {
if ($sourceFilter->getLabel() === $targetFilter->getLabel()) {
$targetFilter->setActive($sourceFilter->isActive());
break;
}
}
}
};
$copyByRangeValue = function (Facet $source, Facet $target) {
foreach ($source->getFilters() as $sourceFilter) {
if ($sourceFilter->isActive()) {
$foundRange = false;
foreach ($target->getFilters() as $targetFilter) {
$tFrom = $targetFilter->getValue()['from'];
$tTo = $targetFilter->getValue()['to'];
$sFrom = $sourceFilter->getValue()['from'];
$sTo = $sourceFilter->getValue()['to'];
if ($tFrom <= $sFrom && $sTo <= $tTo) {
$foundRange = true;
$targetFilter->setActive(true);
break;
}
}
if (!$foundRange) {
$filter = clone $sourceFilter;
$filter->setDisplayed(false);
$target->addFilter($filter);
}
break;
}
}
};
$copy = function (
Facet $source,
Facet $target
) use (
$copyByLabel,
$copyByRangeValue
) {
if ($target->getProperty('range')) {
$strategy = $copyByRangeValue;
} else {
$strategy = $copyByLabel;
}
$strategy($source, $target);
};
foreach ($targetFacets as $targetFacet) {
foreach ($sourceFacets as $sourceFacet) {
if ($sourceFacet->getLabel() === $targetFacet->getLabel()) {
$copy($sourceFacet, $targetFacet);
break;
}
}
}
}
private function getAvailableSortOrders()
{
return [
(new SortOrder('product', 'position', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Relevance', array(), 'Modules.Facetedsearch.Shop')
),
(new SortOrder('product', 'name', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Name, A to Z', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'name', 'desc'))->setLabel(
$this->module->getTranslator()->trans('Name, Z to A', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'price', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Price, low to high', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'price', 'desc'))->setLabel(
$this->module->getTranslator()->trans('Price, high to low', array(), 'Shop.Theme.Catalog')
),
];
}
public function runQuery(
ProductSearchContext $context,
ProductSearchQuery $query
) {
$result = new ProductSearchResult();
$menu = $this->getFacetCollectionFromEncodedFacets($query);
$order_by = $query->getSortOrder()->toLegacyOrderBy(true);
$order_way = $query->getSortOrder()->toLegacyOrderWay();
$facetedSearchFilters = $this->filtersConverter->getFacetedSearchFiltersFromFacets(
$menu->getFacets()
);
$productsAndCount = $this->module->getProductByFilters(
$query->getResultsPerPage(),
$query->getPage(),
$order_by,
$order_way,
$context->getIdLang(),
$facetedSearchFilters
);
$result
->setProducts($productsAndCount['products'])
->setTotalProductsCount($productsAndCount['count'])
->setAvailableSortOrders($this->getAvailableSortOrders())
;
$filterBlock = $this->module->getFilterBlock($facetedSearchFilters);
$facets = $this->filtersConverter->getFacetsFromFacetedSearchFilters(
$filterBlock['filters']
);
$this->copyFiltersActiveState(
$menu->getFacets(),
$facets
);
$this->labelRangeFilters($facets);
$this->addEncodedFacetsToFilters($facets);
$this->hideZeroValues($facets);
$this->hideUselessFacets($facets);
$nextMenu = (new FacetCollection())->setFacets($facets);
$result->setFacetCollection($nextMenu);
$result->setEncodedFacets($this->facetsSerializer->serialize($facets));
return $result;
}
private function labelRangeFilters(array $facets)
{
foreach ($facets as $facet) {
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
foreach ($facet->getFilters() as $filter) {
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($filter->getValue()['from']),
$unit,
Tools::displayNumber($filter->getValue()['to']),
$unit
)
);
}
} elseif ($facet->getType() === 'price') {
foreach ($facet->getFilters() as $filter) {
$filter->setLabel(
sprintf(
'%1$s - %2$s',
Tools::displayPrice($filter->getValue()['from']),
Tools::displayPrice($filter->getValue()['to'])
)
);
}
}
}
}
/**
* 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
$activeFacetFilters = $this->facetsSerializer->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
foreach ($facets as $facet) {
// 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()) {
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($activeFacetFilters, $filter, $facet);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
$facetFilters = $activeFacetFilters;
// toggle the current filter
if ($filter->isActive()) {
$facetFilters = $this->facetsSerializer->removeFilterFromFacetFilters($facetFilters, $filter, $facet);
} else {
$facetFilters = $this->facetsSerializer->addFilterToFacetFilters($facetFilters, $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)
);
}
}
}
private function hideZeroValues(array $facets)
{
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() === 0) {
$filter->setDisplayed(false);
}
}
}
}
private function hideUselessFacets(array $facets)
{
foreach ($facets as $facet) {
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0) {
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
$usefulFiltersCount > 1
);
}
}
}

View File

@@ -0,0 +1,190 @@
<?php
class Ps_FacetedsearchRangeAggregator
{
private function makeNode(array $range, $minColumnIndex, $maxColumnIndex)
{
$min = $range[$minColumnIndex];
$max = $range[$maxColumnIndex];
if ($min === $max) {
$min = $range[$minColumnIndex] > 0 ? $range[$minColumnIndex] - 1 : 0;
$max = $max + 1;
}
return [
'min' => $min,
'max' => $max,
'count' => 1,
'left' => null,
'right' => null,
];
}
private function addNode(array &$target, array $node)
{
if ($node['min'] > $target['max']) {
if ($target['right']) {
$this->addNode($target['right'], $node);
} else {
$target['right'] = $node;
}
} elseif ($node['max'] < $target['min']) {
if ($target['left']) {
$this->addNode($target['left'], $node);
} else {
$target['left'] = $node;
}
} elseif ($node['max'] <= $target['max'] && $node['min'] >= $target['min']) {
++$target['count'];
} else {
$newMin = min($node['min'], $target['min']);
$newMax = max($node['max'], $target['max']);
$target['count'] += $node['count'];
if ($target['left']) {
if ($target['left']['min'] >= $newMin) {
$target['count'] += $target['left']['count'];
$target['left'] = null;
}
}
if ($target['right']) {
if ($target['right']['max'] <= $newMax) {
$target['count'] += $target['right']['count'];
$target['right'] = null;
}
}
$target['min'] = $newMin;
$target['max'] = $newMax;
}
}
public function aggregateRanges(array $ranges, $minColumnIndex, $maxColumnIndex)
{
$rootNode = null;
foreach ($ranges as $range) {
$node = $this->makeNode($range, $minColumnIndex, $maxColumnIndex);
if (null === $rootNode) {
$rootNode = $node;
} else {
$this->addNode($rootNode, $node);
}
}
$flat = $this->flatten($rootNode);
return $flat;
}
private function flatten(array $node)
{
$min = $node['min'];
$max = $node['max'];
$ranges = [[
'min' => $min,
'max' => $max,
'count' => $node['count'],
]];
if ($node['left']) {
$flatLeft = $this->flatten($node['left']);
$min = $flatLeft['min'];
$ranges = array_merge($flatLeft['ranges'], $ranges);
}
if ($node['right']) {
$flatRight = $this->flatten($node['right']);
$max = $flatRight['max'];
$ranges = array_merge($ranges, $flatRight['ranges']);
}
return [
'min' => $min,
'max' => $max,
'ranges' => $ranges,
];
}
public function getRangesFromList(array $list, $valueColumnIndex)
{
$min = null;
$max = null;
$byValue = [];
foreach ($list as $item) {
$n = $item[$valueColumnIndex];
if ($min === null || $n < $min) {
$min = $n;
}
if ($max === null || $n > $max) {
$max = $n;
}
$key = "n$n";
if (!array_key_exists($key, $byValue)) {
$byValue[$key] = [
'count' => 0,
'value' => $n,
];
}
++$byValue[$key]['count'];
}
$ranges = [];
$lastValue = null;
$lastCount = 0;
usort($byValue, function (array $a, array $b) {
return $a['value'] > $b['value'] ? 1 : -1;
});
foreach ($byValue as $countAndValue) {
$value = $countAndValue['value'];
$count = $countAndValue['count'];
if ($lastValue !== null) {
$ranges[] = [
'min' => $lastValue,
'max' => $value,
'count' => $count + $lastCount,
];
} else {
$lastCount = $count;
}
$lastValue = $value;
$lastCount = $count;
}
return [
'min' => $min,
'max' => $max,
'ranges' => $ranges,
];
}
public function mergeRanges(array $ranges, $outputLength)
{
if ($outputLength >= count($ranges)) {
$raw_ranges = $ranges;
} else {
$parts = array_chunk($ranges, floor(count($ranges) / $outputLength));
$raw_ranges = array_map(function (array $ranges) {
$min = $ranges[0]['min'];
$max = $ranges[count($ranges) - 1]['max'];
return [
'min' => $min,
'max' => $max,
'count' => array_reduce($ranges, function ($count, array $range) {
return $count + $range['count'];
}, 0),
];
}, $parts);
}
return $raw_ranges;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
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,140 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
__DIR__,
'..', 'src', 'Ps_FacetedsearchFacetsURLSerializer.php',
]);
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class Ps_FacetedsearchFacetsURLSerializerTest extends PHPUnit_Framework_TestCase
{
private $serializer;
public function setup()
{
$this->serializer = new Ps_FacetedsearchFacetsURLSerializer();
}
public function test_serialize_one_facet()
{
$facet = (new Facet())
->setLabel('Categories')
->addFilter((new Filter())->setLabel('Tops')->setActive(true))
->addFilter((new Filter())->setLabel('Robes')->setActive(true))
;
$this->assertEquals('Categories-Tops-Robes', $this->serializer->serialize([$facet]));
}
public function test_serialize_price_facet()
{
$facet = (new Facet())
->setLabel('Price')
->setProperty('range', true)
->addFilter(
(new Filter())
->setLabel('Doesn\'t matter')
->setActive(true)
->setProperty('symbol', '€')
->setValue(['from' => 7, 'to' => 9])
)
;
$this->assertEquals('Price-€-7-9', $this->serializer->serialize([$facet]));
}
public function test_setFiltersFromEncodedFacets_simple_facets()
{
$template = [
(new Facet())
->setLabel('Categories')
->addFilter((new Filter())->setLabel('Tops')->setActive(false))
->addFilter((new Filter())->setLabel('Dresses')->setActive(false)),
(new Facet())
->setLabel('Strange Birds')
->addFilter((new Filter())->setLabel('Penguins')->setActive(false))
->addFilter((new Filter())->setLabel('Puffins')->setActive(false)),
];
$encodedFacets = 'Categories-Dresses/Strange Birds-Penguins';
$facets = $this
->serializer
->setFiltersFromEncodedFacets(
$template,
$encodedFacets
)
;
/*
* We check that the Dresses filter in the first facet
* and the Pengins filter in the Strange Birds facet were enabled.
*/
$this->assertTrue($facets[0]->getFilters()[1]->isActive());
$this->assertTrue($facets[1]->getFilters()[0]->isActive());
}
public function test_setFiltersFromEncodedFacets_range_filter_adds_the_filter()
{
$template = [
(new Facet())->setLabel('Price')->setProperty('range', true),
];
$encodedFacets = 'Price-€-5-3.14';
$facets = $this
->serializer
->setFiltersFromEncodedFacets(
$template,
$encodedFacets
)
;
$priceFilter = $facets[0]->getFilters()[0];
$this->assertEquals([
'from' => 5,
'to' => 3.14,
], $priceFilter->getValue());
$this->assertTrue($priceFilter->isActive());
}
public function test_setFiltersFromEncodedFacets_range_filter_enables_the_filter()
{
$template = [
(new Facet())->setLabel('Price')->setProperty('range', true)
->addFilter(
(new Filter())
->setActive(false)
->setValue(['from' => 2, 'to' => 7])
),
];
$encodedFacets = 'Price-€-5-3.14';
$facets = $this
->serializer
->setFiltersFromEncodedFacets(
$template,
$encodedFacets
)
;
$filters = $facets[0]->getFilters();
$this->assertCount(1, $filters);
$priceFilter = $filters[0];
$this->assertEquals([
'from' => 2,
'to' => 7,
], $priceFilter->getValue());
$this->assertTrue($priceFilter->isActive());
}
}

View File

@@ -0,0 +1,102 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
__DIR__,
'..', 'src', 'Ps_FacetedsearchRangeAggregator.php',
]);
class Ps_FacetedsearchRangeAggregatorTest extends PHPUnit_Framework_TestCase
{
public function test_ranges_are_aggregated_simple()
{
$ranges = [
['price_min' => 16, 'price_max' => 20],
['price_min' => 26, 'price_max' => 32],
['price_min' => 25, 'price_max' => 31],
['price_min' => 50, 'price_max' => 61],
['price_min' => 28, 'price_max' => 35],
['price_min' => 30, 'price_max' => 37],
['price_min' => 16, 'price_max' => 20],
];
$aggregator = new Ps_FacetedsearchRangeAggregator();
$actual = $aggregator->aggregateRanges($ranges, 'price_min', 'price_max');
$this->assertEquals([
'min' => 16,
'max' => 61,
'ranges' => [
['min' => 16, 'max' => 20, 'count' => 2],
['min' => 25, 'max' => 37, 'count' => 4],
['min' => 50, 'max' => 61, 'count' => 1],
],
], $actual);
}
public function test_ranges_are_aggregated_big_overlap()
{
$ranges = [
['price_min' => 16, 'price_max' => 20],
['price_min' => 26, 'price_max' => 32],
['price_min' => 25, 'price_max' => 31],
['price_min' => 50, 'price_max' => 61],
['price_min' => 28, 'price_max' => 35],
['price_min' => 30, 'price_max' => 37],
['price_min' => 16, 'price_max' => 20],
['price_min' => 25, 'price_max' => 61],
];
$aggregator = new Ps_FacetedsearchRangeAggregator();
$actual = $aggregator->aggregateRanges($ranges, 'price_min', 'price_max');
$this->assertEquals([
'min' => 16,
'max' => 61,
'ranges' => [
['min' => 16, 'max' => 20, 'count' => 2],
['min' => 25, 'max' => 61, 'count' => 6],
],
], $actual);
}
public function test_ranges_are_merged()
{
$ranges = [
['min' => 16, 'max' => 18, 'count' => 1],
['min' => 20, 'max' => 30, 'count' => 3],
['min' => 40, 'max' => 62, 'count' => 5],
['min' => 80, 'max' => 100, 'count' => 7],
['min' => 120, 'max' => 130, 'count' => 9],
['min' => 130, 'max' => 140, 'count' => 11],
];
$aggregator = new Ps_FacetedsearchRangeAggregator();
$actual = $aggregator->mergeRanges($ranges, 3);
$this->assertEquals([
['min' => 10, 'max' => 30, 'count' => 4],
['min' => 30, 'max' => 100, 'count' => 12],
['min' => 100, 'max' => 140, 'count' => 20],
], $actual);
}
public function test_ranges_are_merged_and_max_boudary_is_rounded()
{
$ranges = [
['min' => 10, 'max' => 30, 'count' => 4],
['min' => 30, 'max' => 97, 'count' => 12],
['min' => 100, 'max' => 140, 'count' => 20],
];
$aggregator = new Ps_FacetedsearchRangeAggregator();
$actual = $aggregator->mergeRanges($ranges, 3);
$this->assertEquals([
['min' => 10, 'max' => 30, 'count' => 4],
['min' => 30, 'max' => 100, 'count' => 12],
['min' => 100, 'max' => 140, 'count' => 20],
], $actual);
}
}

View File

@@ -0,0 +1,153 @@
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'],
];
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,135 @@
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,121 @@
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,154 @@
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,39 @@
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,427 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Adapter;
use stdClass;
use Db;
use Context;
use StockAvailable;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL;
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);
}
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_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 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, sa.quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end, cg.id_group, m.name FROM ps_product p STRAIGHT_JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) STRAIGHT_JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) STRAIGHT_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 0 = sa.id_product_attribute ) INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product 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, sa.quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product 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, sa.quantity, p.condition, p.weight, p.price, m.name FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) 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, sa.quantity, p.condition, p.weight, p.price, cp.position FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) 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 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, sa.quantity, psi.price_min, psi.price_max, psi.range_start, psi.range_end FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) INNER JOIN ps_layered_price_index psi ON (psi.id_product = p.id_product AND psi.id_currency = 4 AND psi.id_country = 3) LEFT JOIN ps_stock_available sa1 ON (p.id_product = sa1.id_product AND 0 = sa1.id_product_attribute ) WHERE ((sa.quantity>=0 AND sa1.out_of_stock IN (1, 3, 4)) OR (sa.quantity>0 AND sa1.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, sa.quantity FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) STRAIGHT_JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) STRAIGHT_JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) STRAIGHT_JOIN ps_product_attribute_combination pac1 ON (pa.id_product_attribute = pac1.id_product_attribute) LEFT JOIN ps_stock_available sa1 ON (p.id_product = sa1.id_product AND 0 = sa1.id_product_attribute ) WHERE ((pac.id_attribute=2 AND pac1.id_attribute=4) OR (sa.quantity>0 AND sa1.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, sa.quantity FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) STRAIGHT_JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) STRAIGHT_JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) STRAIGHT_JOIN ps_product_attribute_combination pac1 ON (pa.id_product_attribute = pac1.id_product_attribute) STRAIGHT_JOIN ps_product_attribute_combination pac2 ON (pa.id_product_attribute = pac2.id_product_attribute) LEFT JOIN ps_stock_available sa1 ON (p.id_product = sa1.id_product AND 0 = sa1.id_product_attribute ) WHERE ((pac.id_attribute=2 AND pac1.id_attribute IN (4, 5, 6) AND pac2.id_attribute IN (7, 8, 9)) OR (sa.quantity>0 AND sa1.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 STRAIGHT_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 STRAIGHT_JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) STRAIGHT_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 STRAIGHT_JOIN ps_product_attribute pa ON (p.id_product = pa.id_product) STRAIGHT_JOIN ps_product_attribute_combination pac ON (pa.id_product_attribute = pac.id_product_attribute) STRAIGHT_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_stock_available sa ON (p.id_product = sa.id_product AND 0 = sa.id_product_attribute ) ORDER BY p.id_product DESC LIMIT 0, 20'],
['quantity', 'SELECT sa.quantity FROM ps_product p LEFT JOIN ps_stock_available sa ON (p.id_product = sa.id_product AND 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_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_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_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_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,91 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Tests;
use Context;
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);
$this->module->shouldReceive('getDatabase');
$this->module->shouldReceive('getContext')
->andReturn($contextMock);
$this->dispatcher = new HookDispatcher($this->module);
}
public function testGetAvailableHooks()
{
$this->assertCount(22, $this->dispatcher->getAvailableHooks());
$this->assertEquals(
[
'actionAttributeGroupDelete',
'actionAttributeSave',
'displayAttributeForm',
'actionAttributePostProcess',
'actionAttributeGroupDelete',
'actionAttributeGroupSave',
'displayAttributeGroupForm',
'displayAttributeGroupPostProcess',
'actionCategoryAdd',
'actionCategoryDelete',
'actionCategoryUpdate',
'displayLeftColumn',
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
'actionFeatureValueSave',
'actionFeatureValueDelete',
'displayFeatureValueForm',
'displayFeatureValuePostProcess',
'actionProductSave',
'productSearchProvider',
],
$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,7 @@
<?php
namespace PrestaShop\PrestaShop\Core\Product\Search;
interface FacetsRendererInterface
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\PrestaShop\Core\Product\Search;
interface ProductSearchProviderInterface
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\PrestaShop\Core\Module;
interface WidgetInterface
{
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,344 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Tests\Product;
use Ps_Facetedsearch;
use Tools;
use Db;
use Configuration;
use Context;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
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' => []])
{
$facet = Mockery::mock(Facet::class);
$facet->shouldReceive('getLabel')
->andReturn($label);
$facet->shouldReceive('toArray')
->andReturn($data);
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()
{
$context = Mockery::mock(ProductSearchContext::class);
$productSearchResult = Mockery::mock(ProductSearchResult::class);
$productSearchResult->shouldReceive('getFacetCollection')
->once()
->andReturn(null);
$this->assertEquals(
'',
$this->provider->renderFacets(
$context,
$productSearchResult
)
);
}
public function testRenderFacetsWithFacetsCollection()
{
$context = Mockery::mock(ProductSearchContext::class);
$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('render')
->once()
->with(
'views/templates/front/catalog/facets.tpl',
[
'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',
]
)
->andReturn('');
$this->assertEquals(
'',
$this->provider->renderFacets(
$context,
$productSearchResult
)
);
}
public function testRenderFacetsWithFacetsCollectionAndFilters()
{
$context = Mockery::mock(ProductSearchContext::class);
$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,
],
],
]
);
$this->facetCollection->shouldReceive('getFacets')
->once()
->andReturn(
[
$facet,
]
);
$this->module->shouldReceive('render')
->once()
->with(
'views/templates/front/catalog/facets.tpl',
[
'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',
],
],
],
],
'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',
],
],
],
],
'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',
]
)
->andReturn('');
$this->assertEquals(
'',
$this->provider->renderFacets(
$context,
$productSearchResult
)
);
}
}

View File

@@ -0,0 +1,450 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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 Tools;
use stdClass;
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,
];
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_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' => [
'=' => [
[
6,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_stock_management' => [
[
[
'quantity',
[
0,
],
'<=',
],
[
'out_of_stock',
[
0,
],
'=',
],
],
],
'with_attributes' => [
[
[
'id_attribute',
[
4,
5,
],
],
],
],
'with_features' => [
[
[
'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,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_features' => [
[
[
'id_feature_value',
[
[
1,
],
[
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,
],
],
],
],
$this->search->getSearchAdapter()->getInitialPopulation()->getFilters()->toArray()
);
$this->assertEquals(
[
'with_attributes' => [
[
[
'id_attribute',
[
[
1,
],
[
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,182 @@
<?php
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,15 @@
<?php
// FacetedSearch autoloader
require __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/FacetedSearch/MockProxy.php';
require_once __DIR__ . '/FacetedSearch/Interface/WidgetInterface.php';
require_once __DIR__ . '/FacetedSearch/Interface/FacetsRendererInterface.php';
require_once __DIR__ . '/FacetedSearch/Interface/ProductSearchProviderInterface.php';
// Fake pSQL function
function pSQL($string, $htmlOK = false)
{
return $string;
}

View File

@@ -0,0 +1,55 @@
<?php
$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\\', array('/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,15 @@
parameters:
reportUnmatchedIgnoredErrors: false
bootstrap: /web/module/tests/php/phpstan/bootstrap.php
paths:
- /web/module/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\.~'
level: 5

View File

@@ -0,0 +1,22 @@
<phpunit
bootstrap="bootstrap.php"
>
<php>
<const name="_DB_PREFIX_" value="ps_"/>
<const name="_PS_VERSION_" value="FacetedSearchVersion"/>
<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 processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../../src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,7 @@
<phpunit bootstrap="../../../vendor/autoload.php">
<testsuites>
<testsuite name="FacetedSearch">
<directory>.</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,70 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,49 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderIniteca55e15b41ca8fedd26758ea11dd8fa::getLoader();

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env php
<?php
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
if (getenv('PHP_CS_FIXER_FUTURE_MODE')) {
error_reporting(-1);
}
if (defined('HHVM_VERSION_ID')) {
fwrite(STDERR, "HHVM is not supported.\n");
if (getenv('PHP_CS_FIXER_IGNORE_ENV')) {
fwrite(STDERR, "Ignoring environment requirements because `PHP_CS_FIXER_IGNORE_ENV` is set. Execution may be unstable.\n");
} else {
exit(1);
}
} elseif (!defined('PHP_VERSION_ID') || \PHP_VERSION_ID < 50600 || \PHP_VERSION_ID >= 70400) {
fwrite(STDERR, "PHP needs to be a minimum version of PHP 5.6.0 and maximum version of PHP 7.3.*.\n");
if (getenv('PHP_CS_FIXER_IGNORE_ENV')) {
fwrite(STDERR, "Ignoring environment requirements because `PHP_CS_FIXER_IGNORE_ENV` is set. Execution may be unstable.\n");
} else {
exit(1);
}
}
set_error_handler(function ($severity, $message, $file, $line) {
if ($severity & error_reporting()) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
});
$require = true;
if (class_exists('Phar')) {
// Maybe this file is used as phar-stub? Let's try!
try {
Phar::mapPhar('php-cs-fixer.phar');
require_once 'phar://php-cs-fixer.phar/vendor/autoload.php';
$require = false;
} catch (PharException $e) {
}
}
if ($require) {
// OK, it's not, let give Composer autoloader a try!
if (file_exists($a = __DIR__.'/../../autoload.php')) {
require_once $a;
} else {
require_once __DIR__.'/vendor/autoload.php';
}
unset($a);
}
unset($require);
use Composer\XdebugHandler\XdebugHandler;
use PhpCsFixer\Console\Application;
// Restart if xdebug is loaded, unless the environment variable PHP_CS_FIXER_ALLOW_XDEBUG is set.
$xdebug = new XdebugHandler('PHP_CS_FIXER', '--ansi');
$xdebug->check();
unset($xdebug);
$application = new Application();
$application->run();
__HALT_COMPILER();

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
/**
* PHP Code Beautifier and Fixer fixes violations of a defined coding standard.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
if (is_file(__DIR__.'/../autoload.php') === true) {
include_once __DIR__.'/../autoload.php';
} else {
include_once 'PHP/CodeSniffer/autoload.php';
}
$runner = new PHP_CodeSniffer\Runner();
$exitCode = $runner->runPHPCBF();
exit($exitCode);

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
/**
* PHP_CodeSniffer detects violations of a defined coding standard.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
if (is_file(__DIR__.'/../autoload.php') === true) {
include_once __DIR__.'/../autoload.php';
} else {
include_once 'PHP/CodeSniffer/autoload.php';
}
$runner = new PHP_CodeSniffer\Runner();
$exitCode = $runner->runPHPCS();
exit($exitCode);

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env php
<?php
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (version_compare('5.6.0', PHP_VERSION, '>')) {
fwrite(
STDERR,
sprintf(
'This version of PHPUnit is supported on PHP 5.6, PHP 7.0, and PHP 7.1.' . PHP_EOL .
'You are using PHP %s (%s).' . PHP_EOL,
PHP_VERSION,
PHP_BINARY
)
);
die(1);
}
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) {
if (file_exists($file)) {
define('PHPUNIT_COMPOSER_INSTALL', $file);
break;
}
}
unset($file);
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
fwrite(STDERR,
'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
' composer install' . PHP_EOL . PHP_EOL .
'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
);
die(1);
}
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit_TextUI_Command::main();

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env php
<?php
// OK, it's not, let give Composer autoloader a try!
$autoloadFile = __DIR__.'/../../../autoload.php';
if (file_exists($autoloadFile)) {
require_once $autoloadFile;
} else {
require_once __DIR__.'/../vendor/autoload.php';
}
use Symfony\Component\Console\Application;
use PrestaShop\CodingStandards\Command\CsFixerInitCommand;
use PrestaShop\CodingStandards\Command\PhpStanInitCommand;
$app = new Application();
$app->add(new CsFixerInitCommand());
$app->add(new PhpStanInitCommand());
$app->run();

View File

@@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath.'\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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