├── .eslintignore ├── i18n ├── en_US.csv └── fr_FR.csv ├── doc └── static │ ├── rating_sort.png │ └── rating_filter.png ├── .gitignore ├── .codeclimate.yml ├── registration.php ├── .travis.yml ├── view └── frontend │ ├── web │ ├── template │ │ └── rating-filter.html │ └── css │ │ └── source │ │ └── _module.less │ └── layout │ ├── catalog_category_view_type_layered.xml │ └── catalogsearch_result_index.xml ├── etc ├── elasticsuite_indices.xml ├── module.xml ├── frontend │ └── di.xml └── di.xml ├── Model ├── Layer │ ├── FilterList.php │ └── Filter │ │ └── Rating.php ├── ResourceModel │ └── Product │ │ └── Indexer │ │ └── Fulltext │ │ └── Datasource │ │ └── RatingData.php └── Product │ └── Indexer │ └── Fulltext │ └── Datasource │ └── RatingData.php ├── composer.json ├── Block └── Navigation │ └── Renderer │ └── Rating.php ├── README.md ├── Search └── Request │ └── Product │ └── Attribute │ └── Aggregation │ └── Rating.php ├── Setup ├── InstallData.php ├── UpgradeData.php └── RatingSetup.php ├── Plugin └── Search │ └── Request │ └── Product │ └── Attribute │ └── AggregationResolver.php └── .eslintrc /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /i18n/en_US.csv: -------------------------------------------------------------------------------- 1 | "%1 / 5 and more","%1 / 5 and more" 2 | "%1 / 5","%1 / 5" 3 | "and up","and up" 4 | -------------------------------------------------------------------------------- /i18n/fr_FR.csv: -------------------------------------------------------------------------------- 1 | "%1 / 5 and more","%1 / 5 et plus" 2 | "%1 / 5","%1 / 5" 3 | "and up","et plus" 4 | -------------------------------------------------------------------------------- /doc/static/rating_sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/magento2-module-elasticsuite-rating/HEAD/doc/static/rating_sort.png -------------------------------------------------------------------------------- /doc/static/rating_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/magento2-module-elasticsuite-rating/HEAD/doc/static/rating_filter.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Standard composer ignored paths 2 | composer.phar 3 | /vendor/ 4 | 5 | # Standard IDEs ignored paths 6 | .metadata 7 | *.tmp 8 | *.bak 9 | *.swp 10 | *~.nib 11 | local.properties 12 | .settings/ 13 | .loadpath 14 | .project 15 | .buildpath 16 | .idea/ 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - javascript 8 | - php 9 | eslint: 10 | enabled: true 11 | fixme: 12 | enabled: true 13 | phan: 14 | enabled: true 15 | config: 16 | file_extensions: php 17 | ignore-undeclared: true 18 | ratings: 19 | paths: 20 | - "**.js" 21 | - "**.php" 22 | exclude_paths: 23 | - src/*/Test 24 | - vendor/* 25 | - Resources/* 26 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright 2016 Smile 13 | * @license Open Software License ("OSL") v. 3.0 14 | */ 15 | 16 | \Magento\Framework\Component\ComponentRegistrar::register( 17 | \Magento\Framework\Component\ComponentRegistrar::MODULE, 18 | 'Smile_ElasticsuiteRating', 19 | __DIR__ 20 | ); 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '5.6' 5 | - '7.0' 6 | 7 | install: [ 8 | "mkdir -p app/etc var", 9 | "echo \"{\\\"http-basic\\\":{\\\"repo.magento.com\\\":{\\\"username\\\":\\\"${MAGENTO_USERNAME}\\\",\\\"password\\\":\\\"${MAGENTO_PASSWORD}\\\"}}}\" > auth.json", 10 | "composer install --prefer-dist" 11 | ] 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | 17 | script: 18 | - vendor/bin/phpcs --ignore=/vendor/,/app/ --standard=vendor/smile/magento2-smilelab-phpcs/phpcs-standards/SmileLab --extensions=php ./ 19 | - vendor/bin/phpmd ./ text vendor/smile/magento2-smilelab-phpmd/phpmd-rulesets/rulset.xml --exclude vendor 20 | -------------------------------------------------------------------------------- /view/frontend/web/template/rating-filter.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. 3 | 4 |
    5 |
    6 | 7 |
    8 |
    9 | 10 | 11 | 12 | 13 |
    14 |
  2. 15 |
16 | -------------------------------------------------------------------------------- /view/frontend/web/css/source/_module.less: -------------------------------------------------------------------------------- 1 | // /** 2 | // * DISCLAIMER 3 | // * 4 | // * Do not edit or add to this file if you wish to upgrade this module to newer 5 | // * versions in the future. 6 | // * 7 | // * 8 | // * @category Smile 9 | // * @package Smile\ElasticsuiteRating 10 | // * @author Romain Ruaud 11 | // * @copyright 2017 Smile 12 | // * @license Open Software License ("OSL") v. 3.0 13 | // */ 14 | 15 | .filter-options-content { 16 | a.rating-filter { 17 | &:hover { 18 | padding-top: 5px; 19 | } 20 | 21 | .rating-summary { 22 | display: inline-block; 23 | } 24 | 25 | > span { 26 | vertical-align: top; 27 | padding-top: 2px; 28 | display: inline-block; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /etc/elasticsuite_indices.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /view/frontend/layout/catalog_category_view_type_layered.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /view/frontend/layout/catalogsearch_result_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Model/Layer/FilterList.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Model\Layer; 14 | 15 | /** 16 | * Override of FilterList to add custom renderer for Rating Filter. 17 | * 18 | * @category Smile 19 | * @package Smile\ElasticsuiteRating 20 | * @author Romain Ruaud 21 | */ 22 | class FilterList extends \Smile\ElasticsuiteCatalog\Model\Layer\FilterList 23 | { 24 | /** 25 | * Rating filter name 26 | */ 27 | const RATING_FILTER = 'rating'; 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute) 33 | { 34 | $filterClassName = parent::getAttributeFilterClass($attribute); 35 | 36 | if ($attribute->getAttributeCode() === 'ratings_summary') { 37 | $filterClassName = $this->filterTypes[self::RATING_FILTER]; 38 | } 39 | 40 | return $filterClassName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 23 | 24 | Smile\ElasticsuiteRating\Model\Layer\Filter\Rating 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Smile\ElasticsuiteRating\Model\Layer\Filter\Rating 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "smile/module-elasticsuite-rating", 3 | "type" : "magento2-module", 4 | "license" : "OSL-3.0", 5 | "authors" : [ 6 | { 7 | "name" : "Tony DEPLANQUE", 8 | "email" : "todep@smile.fr" 9 | }, 10 | { 11 | "name" : "Romain Ruaud", 12 | "email" : "romain.ruaud@smile.fr" 13 | } 14 | ], 15 | "description" : "Smile ElasticSuite - Rating search module.", 16 | "homepage" : "https://github.com/Smile-SA/magento2-module-elasticsuite-rating", 17 | "keywords" : [ 18 | "magento", 19 | "magento2", 20 | "elasticsearch", 21 | "search", 22 | "merchandising", 23 | "ratings" 24 | ], 25 | "repositories": [ 26 | { 27 | "type": "composer", 28 | "url": "https://repo.magento.com/" 29 | } 30 | ], 31 | "require" : { 32 | "magento/framework" : ">=102.0.0", 33 | "magento/magento-composer-installer" : "*", 34 | "magento/module-review" : ">=100.1.0", 35 | "smile/elasticsuite" : "^2.8.0" 36 | }, 37 | "require-dev" : { 38 | "smile/magento2-smilelab-quality-suite" : "~2.1.0|~2.2.0" 39 | }, 40 | "autoload" : { 41 | "files" : [ 42 | "registration.php" 43 | ], 44 | "psr-4" : { 45 | "Smile\\ElasticsuiteRating\\" : "" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Smile\ElasticsuiteRating\Model\Product\Indexer\Fulltext\Datasource\RatingData 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Block/Navigation/Renderer/Rating.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Block\Navigation\Renderer; 14 | 15 | /** 16 | * Rating Filter renderer block 17 | * 18 | * @category Smile 19 | * @package Smile\ElasticsuiteRating 20 | * @author Romain Ruaud 21 | */ 22 | class Rating extends \Smile\ElasticsuiteCatalog\Block\Navigation\Renderer\Attribute 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function getJsLayout() 28 | { 29 | $filterItems = $this->getFilter()->getItems(); 30 | 31 | $jsLayoutConfig = [ 32 | 'component' => self::JS_COMPONENT, 33 | 'hasMoreItems' => false, 34 | 'template' => 'Smile_ElasticsuiteRating/rating-filter', 35 | 'maxSize' => count($filterItems), 36 | ]; 37 | 38 | foreach ($filterItems as $item) { 39 | $jsLayoutConfig['items'][] = $item->toArray(['label', 'count', 'url', 'is_selected']); 40 | } 41 | 42 | return json_encode($jsLayoutConfig); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | protected function canRenderFilter() 49 | { 50 | return $this->getFilter() instanceof \Smile\ElasticsuiteRating\Model\Layer\Filter\Rating; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ElasticSuite Ratings 2 | 3 | This module is a plugin for [ElasticSuite](https://github.com/Smile-SA/elasticsuite). 4 | 5 | It allows to display Magento2 user's ratings as a facet filter, and also as a sort order. 6 | 7 | ### Benefits 8 | 9 | - You will see a new facet allowing to **filter on products average ratings**. 10 | 11 | ![Rating Filter](doc/static/rating_filter.png) 12 | 13 | 14 | - You will also be able to use the ratings as a **sort order** in category pages and search results. 15 | 16 | ![Rating Filter](doc/static/rating_sort.png) 17 | 18 | 19 | 20 | ### Requirements 21 | 22 | The module requires : 23 | 24 | - [ElasticSuite](https://github.com/Smile-SA/elasticsuite) > 2.3.* 25 | 26 | - Magento2 CE/EE Edition 27 | 28 | ### Quick Start Guide 29 | 30 | 1. Install the module via Composer : 31 | 32 | ``` composer require smile/module-elasticsuite-rating ``` 33 | 34 | 2. Enable it 35 | 36 | ``` bin/magento module:enable Smile_ElasticsuiteRating ``` 37 | 38 | 3. Install the module and rebuild the DI cache 39 | 40 | ``` bin/magento setup:upgrade ``` 41 | 42 | 4. Process a full reindex of catalogsearch index to reindex the Ratings data 43 | 44 | ``` bin/magento index:reindex catalogsearch_fulltext ``` 45 | 46 | 47 | ### How to use 48 | 49 | The module is adding a new Product Attribute, called **ratings_summary**. 50 | 51 | Since this is managed via an attribute, you are able to : 52 | 53 | - display/hide it on category pages (via the __Is Filterable__ option) 54 | - display/hide it on search pages (via the __Is Filterable In Search__ option) 55 | - allow/disallow it for sort order (via the __Used for Sort By__ option) 56 | -------------------------------------------------------------------------------- /Model/ResourceModel/Product/Indexer/Fulltext/Datasource/RatingData.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2016 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Model\ResourceModel\Product\Indexer\Fulltext\Datasource; 14 | 15 | use Magento\Framework\App\ResourceConnection; 16 | use Magento\Store\Model\StoreManagerInterface; 17 | use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer; 18 | 19 | /** 20 | * Catalog Rating Data source resource model 21 | * 22 | * @category Smile 23 | * @package Smile\ElasticsuiteRating 24 | * @author Tony DEPLANQUE 25 | */ 26 | class RatingData extends Indexer 27 | { 28 | /** 29 | * Load rating data for a list of product ids and a given store. 30 | * 31 | * @param integer $storeId Store id. 32 | * @param array $productIds Product ids list. 33 | * 34 | * @return array 35 | */ 36 | public function loadRatingData($storeId, $productIds) 37 | { 38 | $select = $this->getConnection()->select() 39 | ->from( 40 | ['res' => $this->getTable('review_entity_summary')], 41 | [ 42 | 'entity_pk_value as product_id', 43 | 'avg(rating_summary) as ratings_summary', 44 | ] 45 | ) 46 | ->where('res.store_id = ?', $storeId) 47 | ->where('res.entity_pk_value IN(?)', $productIds) 48 | ->group('entity_pk_value'); 49 | 50 | return $this->getConnection()->fetchAll($select); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Search/Request/Product/Attribute/Aggregation/Rating.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2019 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | 14 | namespace Smile\ElasticsuiteRating\Search\Request\Product\Attribute\Aggregation; 15 | 16 | use Smile\ElasticsuiteCatalog\Search\Request\Product\Attribute\AggregationInterface; 17 | use Smile\ElasticsuiteCore\Search\Request\BucketInterface; 18 | 19 | /** 20 | * Aggregation builder for product ratings. 21 | * 22 | * @category Smile 23 | * @package Smile\ElasticsuiteRating 24 | * @author Romain Ruaud 25 | */ 26 | class Rating implements AggregationInterface 27 | { 28 | /** 29 | * Default interval, based on 0-100 divided in five stars. 30 | */ 31 | const RATING_AGG_INTERVAL = 20; 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getAggregationData(\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute) 37 | { 38 | $bucketConfig = [ 39 | 'name' => $this->getFilterField($attribute), 40 | 'type' => BucketInterface::TYPE_HISTOGRAM, 41 | 'minDocCount' => 1, 42 | 'interval' => (int) self::RATING_AGG_INTERVAL, 43 | ]; 44 | 45 | return $bucketConfig; 46 | } 47 | 48 | /** 49 | * Retrieve ES filter field. 50 | * 51 | * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute Attribute 52 | * 53 | * @return string 54 | */ 55 | private function getFilterField(\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute) 56 | { 57 | $field = $attribute->getAttributeCode(); 58 | 59 | return $field; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Setup/InstallData.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Setup; 14 | 15 | use Magento\Framework\Setup\InstallDataInterface; 16 | use Magento\Framework\Setup\ModuleContextInterface; 17 | use Magento\Framework\Setup\ModuleDataSetupInterface; 18 | use Magento\Eav\Setup\EavSetupFactory; 19 | 20 | /** 21 | * ElasticsuiteRating Install Data Script. 22 | * 23 | * @category Smile 24 | * @package Smile\ElasticsuiteRating 25 | * @author Romain Ruaud 26 | */ 27 | class InstallData implements InstallDataInterface 28 | { 29 | /** 30 | * @var \Magento\Eav\Setup\EavSetupFactory 31 | */ 32 | private $eavSetupFactory; 33 | 34 | /** 35 | * @var \Smile\ElasticsuiteRating\Setup\RatingSetup 36 | */ 37 | private $ratingSetup; 38 | 39 | /** 40 | * InstallData constructor. 41 | * 42 | * @param \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory EAV Setup Factory 43 | * @param \Smile\ElasticsuiteRating\Setup\RatingSetup $ratingSetup Rating Setup 44 | */ 45 | public function __construct(EavSetupFactory $eavSetupFactory, RatingSetup $ratingSetup) 46 | { 47 | $this->eavSetupFactory = $eavSetupFactory; 48 | $this->ratingSetup = $ratingSetup; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) 55 | { 56 | $setup->startSetup(); 57 | 58 | $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); 59 | 60 | $this->ratingSetup->createRatingAttributes($eavSetup); 61 | 62 | $setup->endSetup(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Setup/UpgradeData.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Setup; 14 | 15 | use Magento\Framework\Setup\ModuleContextInterface; 16 | use Magento\Framework\Setup\ModuleDataSetupInterface; 17 | use Magento\Framework\Setup\UpgradeDataInterface; 18 | use Magento\Eav\Setup\EavSetupFactory; 19 | 20 | /** 21 | * ElasticsuiteRating Upgrade Data Script. 22 | * 23 | * @category Smile 24 | * @package Smile\ElasticsuiteRating 25 | * @author Romain Ruaud 26 | */ 27 | class UpgradeData implements UpgradeDataInterface 28 | { 29 | /** 30 | * @var \Magento\Eav\Setup\EavSetupFactory 31 | */ 32 | private $eavSetupFactory; 33 | 34 | /** 35 | * @var \Smile\ElasticsuiteRating\Setup\RatingSetup 36 | */ 37 | private $ratingSetup; 38 | 39 | /** 40 | * UpgradeData constructor. 41 | * 42 | * @param \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory EAV Setup Factory 43 | * @param \Smile\ElasticsuiteRating\Setup\RatingSetup $ratingSetup Rating Setup 44 | */ 45 | public function __construct(EavSetupFactory $eavSetupFactory, RatingSetup $ratingSetup) 46 | { 47 | $this->eavSetupFactory = $eavSetupFactory; 48 | $this->ratingSetup = $ratingSetup; 49 | } 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) 54 | { 55 | $setup->startSetup(); 56 | 57 | if (version_compare($context->getVersion(), '1.1.0', '<')) { 58 | $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); 59 | $this->ratingSetup->createRatingAttributes($eavSetup); 60 | } 61 | if (version_compare($context->getVersion(), '1.2.0', '<')) { 62 | $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); 63 | $this->ratingSetup->renameRatingAttribute($eavSetup); 64 | } 65 | 66 | $setup->endSetup(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Plugin/Search/Request/Product/Attribute/AggregationResolver.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2019 Smile 12 | * @license Open Software License ("OSL") v. 3.0 13 | */ 14 | namespace Smile\ElasticsuiteRating\Plugin\Search\Request\Product\Attribute; 15 | 16 | /** 17 | * Plugin to set aggregation builder for ratings. 18 | * 19 | * @category Smile 20 | * @package Smile\ElasticsuiteRating 21 | * @author Romain Ruaud 22 | */ 23 | class AggregationResolver 24 | { 25 | /** 26 | * Rating Summary attribute code. 27 | */ 28 | const RATING_SUMMARY_ATTRIBUTE = 'ratings_summary'; 29 | 30 | /** 31 | * @var \Smile\ElasticsuiteRating\Search\Request\Product\Attribute\Aggregation\Rating 32 | */ 33 | private $ratingAggregation; 34 | 35 | /** 36 | * AggregationResolver constructor. 37 | * 38 | * @param \Smile\ElasticsuiteRating\Search\Request\Product\Attribute\Aggregation\Rating $ratingAggregation Rating Aggregation 39 | */ 40 | public function __construct(\Smile\ElasticsuiteRating\Search\Request\Product\Attribute\Aggregation\Rating $ratingAggregation) 41 | { 42 | $this->ratingAggregation = $ratingAggregation; 43 | } 44 | 45 | /** 46 | * Set default facet size to 0 for swatches attributes before adding it as aggregation. 47 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 48 | * 49 | * @param \Smile\ElasticsuiteCatalog\Search\Request\Product\Attribute\AggregationResolver $subject Aggregation Resolver 50 | * @param array $result Aggregation Config 51 | * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute Attribute 52 | * 53 | * @return array 54 | */ 55 | public function afterGetAggregationData( 56 | \Smile\ElasticsuiteCatalog\Search\Request\Product\Attribute\AggregationResolver $subject, 57 | $result, 58 | $attribute 59 | ) { 60 | if ($attribute->getAttributeCode() === self::RATING_SUMMARY_ATTRIBUTE) { 61 | $result = $this->ratingAggregation->getAggregationData($attribute); 62 | } 63 | 64 | return $result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Model/Product/Indexer/Fulltext/Datasource/RatingData.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2016 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Model\Product\Indexer\Fulltext\Datasource; 14 | 15 | use Smile\ElasticsuiteCore\Api\Index\DatasourceInterface; 16 | use Smile\ElasticsuiteRating\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\RatingData as ResourceModel; 17 | 18 | /** 19 | * Ratings Datasource 20 | * 21 | * @category Smile 22 | * @package Smile\ElasticsuiteRating 23 | * @author Tony DEPLANQUE 24 | */ 25 | class RatingData implements DatasourceInterface 26 | { 27 | /** 28 | * @var ResourceModel 29 | */ 30 | private $resourceModel; 31 | 32 | /** 33 | * Constructor. 34 | * 35 | * @param ResourceModel $resourceModel Resource model. 36 | */ 37 | public function __construct(ResourceModel $resourceModel) 38 | { 39 | $this->resourceModel = $resourceModel; 40 | } 41 | 42 | /** 43 | * Add rating data to the index data. 44 | * 45 | * {@inheritdoc} 46 | */ 47 | public function addData($storeId, array $indexData) 48 | { 49 | $ratingData = $this->resourceModel->loadRatingData($storeId, array_keys($indexData)); 50 | 51 | array_walk($indexData, [$this, 'fillRatingsData']); 52 | 53 | foreach ($ratingData as $ratingDataRow) { 54 | $productId = (int) $ratingDataRow['product_id']; 55 | $indexData[$productId]['ratings_summary'] = (float) $ratingDataRow['ratings_summary']; 56 | 57 | if (!isset($indexData[$productId]['indexed_attributes'])) { 58 | $indexData[$productId]['indexed_attributes'] = ['ratings_summary']; 59 | } elseif (!in_array('ratings_summary', $indexData[$productId]['indexed_attributes'])) { 60 | // Add ratings_summary only one time. 61 | $indexData[$productId]['indexed_attributes'][] = 'ratings_summary'; 62 | } 63 | } 64 | 65 | return $indexData; 66 | } 67 | 68 | /** 69 | * Fill rating summary field with 0. 70 | * 71 | * @SuppressWarnings(PHPMD.UnusedPrivateMethod) Used via a callback. 72 | * 73 | * @param array $productData Product index data 74 | */ 75 | private function fillRatingsData(&$productData) 76 | { 77 | $productData['ratings_summary'] = 0; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Setup/RatingSetup.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | namespace Smile\ElasticsuiteRating\Setup; 14 | 15 | use Magento\Catalog\Api\Data\ProductAttributeInterface; 16 | 17 | /** 18 | * ElasticsuiteRating Setup 19 | * 20 | * @category Smile 21 | * @package Smile\ElasticsuiteRating 22 | * @author Romain Ruaud 23 | */ 24 | class RatingSetup 25 | { 26 | /** 27 | * @var \Magento\Eav\Model\Config $eavConfig 28 | */ 29 | private $eavConfig; 30 | 31 | /** 32 | * VirtualCategorySetup constructor. 33 | * 34 | * @param \Magento\Eav\Model\Config $eavConfig EAV Config. 35 | */ 36 | public function __construct(\Magento\Eav\Model\Config $eavConfig) 37 | { 38 | $this->eavConfig = $eavConfig; 39 | } 40 | 41 | /** 42 | * Create product rating attribute. 43 | * 44 | * @param \Magento\Eav\Setup\EavSetup $eavSetup EAV module Setup 45 | */ 46 | public function createRatingAttributes($eavSetup) 47 | { 48 | $entity = ProductAttributeInterface::ENTITY_TYPE_CODE; 49 | $eavSetup->addAttribute( 50 | $entity, 51 | 'ratings_summary', 52 | [ 53 | 'type' => 'decimal', 54 | 'label' => 'Product Rating', 55 | 'input' => 'hidden', 56 | 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, 57 | 'required' => false, 58 | 'default' => 0, 59 | 'visible' => true, 60 | 'sort_order' => 200, 61 | 'visible_on_front' => 0, 62 | 'searchable' => 1, 63 | 'visible_in_advanced_search' => 0, 64 | 'filterable' => 1, 65 | 'filterable_in_search' => 1, 66 | 'is_used_in_grid' => 0, 67 | 'is_visible_in_grid' => 0, 68 | 'is_filterable_in_grid' => 0, 69 | 'used_for_sort_by' => 1, 70 | ] 71 | ); 72 | 73 | $attributeId = $eavSetup->getAttributeId($entity, 'ratings_summary'); 74 | $defaultAttributeSet = $eavSetup->getAttributeSetId($entity, 'Default'); 75 | $defaultGroup = $eavSetup->getAttributeGroupId($entity, $defaultAttributeSet, 'General'); 76 | 77 | $eavSetup->addAttributeToSet($entity, $defaultAttributeSet, $defaultGroup, $attributeId); 78 | } 79 | 80 | /** 81 | * Rename rating_summary to ratings_summary 82 | * 83 | * @param \Magento\Eav\Setup\EavSetup $eavSetup EAV module Setup 84 | */ 85 | public function renameRatingAttribute($eavSetup) 86 | { 87 | $entity = ProductAttributeInterface::ENTITY_TYPE_CODE; 88 | if ($eavSetup->getAttributeId($entity, 'rating_summary') !== false) { 89 | $eavSetup->updateAttribute($entity, 'rating_summary', ['attribute_code' => 'ratings_summary']); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Rating.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2017 Smile 11 | * @license Open Software License ("OSL") v. 3.0 12 | */ 13 | 14 | namespace Smile\ElasticsuiteRating\Model\Layer\Filter; 15 | 16 | /** 17 | * Products Rating Filter Model 18 | * 19 | * @category Smile 20 | * @package Smile\ElasticsuiteRating 21 | * @author Romain Ruaud 22 | */ 23 | class Rating extends \Smile\ElasticsuiteCatalog\Model\Layer\Filter\Attribute 24 | { 25 | /** 26 | * Default interval, based on 0-100 divided in five stars. 27 | */ 28 | const RATING_AGG_INTERVAL = 20; 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | public function apply(\Magento\Framework\App\RequestInterface $request) 34 | { 35 | $value = $request->getParam($this->_requestVar); 36 | 37 | if (null !== $value) { 38 | $this->currentFilterValue = $value; 39 | 40 | /** @var \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Fulltext\Collection $productCollection */ 41 | $productCollection = $this->getLayer()->getProductCollection(); 42 | 43 | $productCollection->addFieldToFilter($this->getFilterField(), ['gte' => $value]); 44 | $layerState = $this->getLayer()->getState(); 45 | 46 | $filterLabel = __('%1 / 5 and more', $value / self::RATING_AGG_INTERVAL); 47 | if (($value / self::RATING_AGG_INTERVAL) === 5) { 48 | $filterLabel = __('%1 / 5', $value / self::RATING_AGG_INTERVAL); 49 | } 50 | $filter = $this->_createItem($filterLabel, $this->currentFilterValue); 51 | 52 | $layerState->addFilter($filter); 53 | } 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Retrieve ES filter field. 60 | * 61 | * @return string 62 | */ 63 | protected function getFilterField() 64 | { 65 | $field = $this->getAttributeModel()->getAttributeCode(); 66 | 67 | return $field; 68 | } 69 | 70 | /** 71 | * @SuppressWarnings(PHPMD.CamelCaseMethodName) 72 | * {@inheritDoc} 73 | */ 74 | protected function _initItems() 75 | { 76 | $data = $this->_getItemsData(); 77 | $items = []; 78 | foreach ($data as $itemData) { 79 | $items[] = $this->_createItem($itemData['label'], $itemData['value'], $itemData['count']); 80 | } 81 | $this->_items = $items; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @SuppressWarnings(PHPMD.CamelCaseMethodName) 88 | * {@inheritDoc} 89 | */ 90 | protected function _getItemsData() 91 | { 92 | /** @var \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Fulltext\Collection $productCollection */ 93 | $productCollection = $this->getLayer()->getProductCollection(); 94 | 95 | $optionsFacetedData = $productCollection->getFacetedData($this->getFilterField()); 96 | 97 | $items = []; 98 | 99 | $sumCount = 0; 100 | $maxValue = current(array_keys($optionsFacetedData)); 101 | 102 | while (($maxValue = $maxValue - self::RATING_AGG_INTERVAL) && $maxValue > 0) { 103 | if (!isset($optionsFacetedData[$maxValue])) { 104 | $optionsFacetedData[$maxValue] = ['count' => 0]; 105 | } 106 | } 107 | krsort($optionsFacetedData); 108 | 109 | $minCount = !empty($optionsFacetedData) ? min(array_column($optionsFacetedData, 'count')) : 0; 110 | 111 | if (!empty($this->currentFilterValue) || $minCount < $productCollection->getSize()) { 112 | foreach ($optionsFacetedData as $value => $data) { 113 | $sumCount += (int) $data['count']; 114 | $items[$value] = [ 115 | 'label' => $value, 116 | 'value' => $value, 117 | 'count' => $sumCount, 118 | ]; 119 | } 120 | } 121 | 122 | return $items; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | modules: true 3 | jsx: true 4 | 5 | env: 6 | amd: true 7 | browser: true 8 | es6: true 9 | jquery: true 10 | node: true 11 | 12 | # http://eslint.org/docs/rules/ 13 | rules: 14 | # Possible Errors 15 | comma-dangle: [2, never] 16 | no-cond-assign: 2 17 | no-console: 0 18 | no-constant-condition: 2 19 | no-control-regex: 2 20 | no-debugger: 2 21 | no-dupe-args: 2 22 | no-dupe-keys: 2 23 | no-duplicate-case: 2 24 | no-empty: 2 25 | no-empty-character-class: 2 26 | no-ex-assign: 2 27 | no-extra-boolean-cast: 2 28 | no-extra-parens: 0 29 | no-extra-semi: 2 30 | no-func-assign: 2 31 | no-inner-declarations: [2, functions] 32 | no-invalid-regexp: 2 33 | no-irregular-whitespace: 2 34 | no-negated-in-lhs: 2 35 | no-obj-calls: 2 36 | no-regex-spaces: 2 37 | no-sparse-arrays: 2 38 | no-unexpected-multiline: 2 39 | no-unreachable: 2 40 | use-isnan: 2 41 | valid-jsdoc: 0 42 | valid-typeof: 2 43 | 44 | # Best Practices 45 | accessor-pairs: 2 46 | block-scoped-var: 0 47 | complexity: [2, 6] 48 | consistent-return: 0 49 | curly: 0 50 | default-case: 0 51 | dot-location: 0 52 | dot-notation: 0 53 | eqeqeq: 2 54 | guard-for-in: 2 55 | no-alert: 2 56 | no-caller: 2 57 | no-case-declarations: 2 58 | no-div-regex: 2 59 | no-else-return: 0 60 | no-empty-label: 2 61 | no-empty-pattern: 2 62 | no-eq-null: 2 63 | no-eval: 2 64 | no-extend-native: 2 65 | no-extra-bind: 2 66 | no-fallthrough: 2 67 | no-floating-decimal: 0 68 | no-implicit-coercion: 0 69 | no-implied-eval: 2 70 | no-invalid-this: 0 71 | no-iterator: 2 72 | no-labels: 0 73 | no-lone-blocks: 2 74 | no-loop-func: 2 75 | no-magic-number: 0 76 | no-multi-spaces: 0 77 | no-multi-str: 0 78 | no-native-reassign: 2 79 | no-new-func: 2 80 | no-new-wrappers: 2 81 | no-new: 2 82 | no-octal-escape: 2 83 | no-octal: 2 84 | no-proto: 2 85 | no-redeclare: 2 86 | no-return-assign: 2 87 | no-script-url: 2 88 | no-self-compare: 2 89 | no-sequences: 0 90 | no-throw-literal: 0 91 | no-unused-expressions: 2 92 | no-useless-call: 2 93 | no-useless-concat: 2 94 | no-void: 2 95 | no-warning-comments: 0 96 | no-with: 2 97 | radix: 2 98 | vars-on-top: 0 99 | wrap-iife: 2 100 | yoda: 0 101 | 102 | # Strict 103 | strict: 0 104 | 105 | # Variables 106 | init-declarations: 0 107 | no-catch-shadow: 2 108 | no-delete-var: 2 109 | no-label-var: 2 110 | no-shadow-restricted-names: 2 111 | no-shadow: 0 112 | no-undef-init: 2 113 | no-undef: 0 114 | no-undefined: 0 115 | no-unused-vars: 0 116 | no-use-before-define: 0 117 | 118 | # Node.js and CommonJS 119 | callback-return: 2 120 | global-require: 2 121 | handle-callback-err: 2 122 | no-mixed-requires: 0 123 | no-new-require: 0 124 | no-path-concat: 2 125 | no-process-exit: 2 126 | no-restricted-modules: 0 127 | no-sync: 0 128 | 129 | # Stylistic Issues 130 | array-bracket-spacing: 0 131 | block-spacing: 0 132 | brace-style: 0 133 | camelcase: 0 134 | comma-spacing: 0 135 | comma-style: 0 136 | computed-property-spacing: 0 137 | consistent-this: 0 138 | eol-last: 0 139 | func-names: 0 140 | func-style: 0 141 | id-length: 0 142 | id-match: 0 143 | indent: 0 144 | jsx-quotes: 0 145 | key-spacing: 0 146 | linebreak-style: 0 147 | lines-around-comment: 0 148 | max-depth: 0 149 | max-len: 0 150 | max-nested-callbacks: 0 151 | max-params: 0 152 | max-statements: [2, 30] 153 | new-cap: 0 154 | new-parens: 0 155 | newline-after-var: 0 156 | no-array-constructor: 0 157 | no-bitwise: 0 158 | no-continue: 0 159 | no-inline-comments: 0 160 | no-lonely-if: 0 161 | no-mixed-spaces-and-tabs: 0 162 | no-multiple-empty-lines: 0 163 | no-negated-condition: 0 164 | no-nested-ternary: 0 165 | no-new-object: 0 166 | no-plusplus: 0 167 | no-restricted-syntax: 0 168 | no-spaced-func: 0 169 | no-ternary: 0 170 | no-trailing-spaces: 0 171 | no-underscore-dangle: 0 172 | no-unneeded-ternary: 0 173 | object-curly-spacing: 0 174 | one-var: 0 175 | operator-assignment: 0 176 | operator-linebreak: 0 177 | padded-blocks: 0 178 | quote-props: 0 179 | quotes: 0 180 | require-jsdoc: 0 181 | semi-spacing: 0 182 | semi: 0 183 | sort-vars: 0 184 | space-after-keywords: 0 185 | space-before-blocks: 0 186 | space-before-function-paren: 0 187 | space-before-keywords: 0 188 | space-in-parens: 0 189 | space-infix-ops: 0 190 | space-return-throw-case: 0 191 | space-unary-ops: 0 192 | spaced-comment: 0 193 | wrap-regex: 0 194 | 195 | # ECMAScript 6 196 | arrow-body-style: 0 197 | arrow-parens: 0 198 | arrow-spacing: 0 199 | constructor-super: 0 200 | generator-star-spacing: 0 201 | no-arrow-condition: 0 202 | no-class-assign: 0 203 | no-const-assign: 0 204 | no-dupe-class-members: 0 205 | no-this-before-super: 0 206 | no-var: 0 207 | object-shorthand: 0 208 | prefer-arrow-callback: 0 209 | prefer-const: 0 210 | prefer-reflect: 0 211 | prefer-spread: 0 212 | prefer-template: 0 213 | require-yield: 0 214 | --------------------------------------------------------------------------------