diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue.php new file mode 100644 index 00000000..36b5b7b1 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue.php @@ -0,0 +1,43 @@ +_blockGroup = 'algoliasearch'; + $this->_controller = 'adminhtml_indexingqueue'; + + parent::__construct(); + + $this->_removeButton('add'); + $this->_addButton('clear_queue', array( + 'label' => Mage::helper('algoliasearch')->__('Clear Queue'), + 'onclick' => "if (confirm('Are you sure you want to clear the queue? This operation cannot be reverted.')) { + location.href='" . $this->getUrl('*/*/clear') . "' };", + 'class' => 'cancel', + )); + } + + /** + * Get header text. + * + * @return string + */ + public function getHeaderText() + { + return Mage::helper('algoliasearch')->__('Algolia Search - Indexing Queue'); + } + + /** + * Set custom Algolia icon class. + * + * @return string + */ + public function getHeaderCssClass() + { + return 'icon-head algoliasearch-head-icon'; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit.php new file mode 100644 index 00000000..473e18a5 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit.php @@ -0,0 +1,41 @@ +_objectId = 'job_id'; + $this->_blockGroup = 'algoliasearch'; + $this->_controller = 'adminhtml_indexingqueue'; + + $this->_removeButton('save'); + $this->_removeButton('reset'); + $this->_removeButton('delete'); + } + + /** + * Get header text. + * + * @return string + */ + public function getHeaderText() + { + return Mage::helper('algoliasearch')->__('Algolia Search - Indexing Queue Job #%s', + Mage::registry('algoliasearch_current_job')->getJobId()); + } + + /** + * Set custom Algolia icon class. + * + * @return string + */ + public function getHeaderCssClass() + { + return 'icon-head algoliasearch-head-icon'; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit/Form.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit/Form.php new file mode 100644 index 00000000..29d405df --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Edit/Form.php @@ -0,0 +1,118 @@ + 'edit_form', + 'action' => $this->getUrl('*/*/updatePost'), + 'method' => 'post', + )); + + $fieldset = $form->addFieldset('base_fieldset', array()); + $readOnlyStyle = 'border: 0; background: none;'; + + $fieldset->addField('job_id', 'text', array( + 'name' => 'job_id', + 'label' => Mage::helper('algoliasearch')->__('Job ID'), + 'title' => Mage::helper('algoliasearch')->__('Job ID'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('created', 'text', array( + 'name' => 'created', + 'label' => Mage::helper('algoliasearch')->__('Created'), + 'title' => Mage::helper('algoliasearch')->__('Created'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('status', 'text', array( + 'name' => 'status', + 'label' => Mage::helper('algoliasearch')->__('Status'), + 'title' => Mage::helper('algoliasearch')->__('Status'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('pid', 'text', array( + 'name' => 'pid', + 'label' => Mage::helper('algoliasearch')->__('PID'), + 'title' => Mage::helper('algoliasearch')->__('PID'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('class', 'text', array( + 'name' => 'class', + 'label' => Mage::helper('algoliasearch')->__('Class'), + 'title' => Mage::helper('algoliasearch')->__('Class'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('method', 'text', array( + 'name' => 'method', + 'label' => Mage::helper('algoliasearch')->__('Method'), + 'title' => Mage::helper('algoliasearch')->__('Method'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('data', 'textarea', array( + 'name' => 'data', + 'label' => Mage::helper('algoliasearch')->__('Data'), + 'title' => Mage::helper('algoliasearch')->__('Data'), + 'readonly' => true, + )); + + $fieldset->addField('max_retries', 'text', array( + 'name' => 'max_retries', + 'label' => Mage::helper('algoliasearch')->__('Max Retries'), + 'title' => Mage::helper('algoliasearch')->__('Max Retries'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('retries', 'text', array( + 'name' => 'retries', + 'label' => Mage::helper('algoliasearch')->__('Retries'), + 'title' => Mage::helper('algoliasearch')->__('Retries'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('data_size', 'text', array( + 'name' => 'data_size', + 'label' => Mage::helper('algoliasearch')->__('Data Size'), + 'title' => Mage::helper('algoliasearch')->__('Data Size'), + 'readonly' => true, + 'style' => $readOnlyStyle, + )); + + $fieldset->addField('error_log', 'textarea', array( + 'name' => 'error_log', + 'label' => Mage::helper('algoliasearch')->__('Error Log'), + 'title' => Mage::helper('algoliasearch')->__('Error Log'), + 'readonly' => true, + )); + + + $form->setValues($model->getData()); + $form->addValues(array( + 'status' => $model->getStatusLabel() + )); + $form->setUseContainer(true); + + $this->setForm($form); + + return parent::_prepareForm(); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid.php new file mode 100644 index 00000000..2b317b39 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid.php @@ -0,0 +1,116 @@ +setId('job_id'); + $this->setDefaultSort('job_id'); + $this->setDefaultDir('acs'); + } + + /** + * Prepare Search Report collection for grid + * + * @return Mage_Adminhtml_Block_Report_Search_Grid + */ + protected function _prepareCollection() + { + $collection = Mage::getResourceModel('algoliasearch/job_collection'); + $this->setCollection($collection); + + return parent::_prepareCollection(); + } + + /** + * Prepare Grid columns + * + * @return Mage_Adminhtml_Block_Report_Search_Grid + */ + protected function _prepareColumns() + { + $this->addColumn('job_id', array( + 'header' => Mage::helper('algoliasearch')->__('Job ID'), + 'width' => '50px', + 'filter' => false, + 'index' => 'job_id', + 'type' => 'number' + )); + + $this->addColumn('created', array( + 'header' => Mage::helper('algoliasearch')->__('Created'), + 'index' => 'created', + 'type' => 'datetime', + )); + + $this->addColumn('status', array( + 'header' => Mage::helper('algoliasearch')->__('Status'), + 'index' => 'status', + 'getter' => 'getStatusLabel', + 'filter' => false, + )); + + $this->addColumn('method', array( + 'header' => Mage::helper('algoliasearch')->__('Method'), + 'index' => 'method', + 'type' => 'options', + 'options' => Mage::getModel('algoliasearch/source_jobMethods')->getMethods(), + )); + + $this->addColumn('data', array( + 'header' => Mage::helper('algoliasearch')->__('Data'), + 'index' => 'data', + 'renderer' => 'Algolia_Algoliasearch_Block_Adminhtml_IndexingQueue_Grid_Renderer_Json' + )); + + $this->addColumn('max_retries', array( + 'header' => Mage::helper('algoliasearch')->__('Max Retries'), + 'width' => '40px', + 'filter' => false, + 'index' => 'max_retries', + 'type' => 'number' + )); + + $this->addColumn('retries', array( + 'header' => Mage::helper('algoliasearch')->__('Retries'), + 'width' => '40px', + 'filter' => false, + 'index' => 'retries', + 'type' => 'number' + )); + + $this->addColumn('action', + array( + 'header' => Mage::helper('algoliasearch')->__('Action'), + 'width' => '50px', + 'type' => 'action', + 'getter' => 'getJobId', + 'actions' => array( + array( + 'caption' => Mage::helper('algoliasearch')->__('View'), + 'url' => array('base'=>'*/*/view'), + 'field' => 'id' + ) + ), + 'filter' => false, + 'sortable' => false, + )); + + return parent::_prepareColumns(); + } + + /** + * Retrieve Row Click callback URL + * + * @return string + */ + public function getRowUrl($row) + { + return $this->getUrl('*/*/view', array('id' => $row->getJobId())); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid/Renderer/Json.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid/Renderer/Json.php new file mode 100644 index 00000000..7a6b913c --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Grid/Renderer/Json.php @@ -0,0 +1,21 @@ +getData('data')) { + $json = json_decode($json, true); + + foreach ($json as $var => $value) { + $html .= $var . ': ' . (is_array($value) ? implode(',', $value) : $value) . '
'; + } + } + return $html; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Status.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Status.php new file mode 100644 index 00000000..25068c1c --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/IndexingQueue/Status.php @@ -0,0 +1,176 @@ +config = Mage::helper('algoliasearch/config'); + $this->dateTime = Mage::getModel('core/date'); + $this->queue = Mage::getModel('algoliasearch/queue'); + + $this->queueRunnerIndexer = Mage::getModel('index/indexer') + ->getProcessByCode(Algolia_Algoliasearch_Model_Indexer_Algoliaqueuerunner::INDEXER_ID); + + $this->setTemplate('algoliasearch/queue/status.phtml'); + } + + /** + * @return mixed + */ + public function isQueueActive() + { + return $this->config->isQueueActive(); + } + + /** + * @return string + */ + public function getQueueRunnerStatus() + { + $status = 'Unknown'; + + /** @var Mage_Index_Model_Process $process */ + $process = Mage::getModel('index/process'); + $statuses = $process->getStatusesOptions(); + + if ($this->queueRunnerIndexer->getStatus() + && isset($statuses[$this->queueRunnerIndexer->getStatus()])) { + $status = $statuses[$this->queueRunnerIndexer->getStatus()]; + } + + return $status; + } + + public function getLastQueueUpdate() + { + return $this->queueRunnerIndexer->getEndedAt(); + } + + /** + * @return mixed + */ + public function getResetQueueUrl() + { + return $this->getUrl('*/*/reset'); + } + + /** + * @return array + */ + public function getNotices() + { + $notices = array(); + + if ($this->isQueueStuck()) { + $notices[] = ' ' . $this->__('Reset Queue') . ''; + } + + if ($this->isQueueNotProcessed()) { + $notices[] = $this->__( + 'Queue has not been processed for one hour and indexing might be stuck or your cron is not set up properly.' + ); + $notices[] = $this->__( + 'To help you, please read our documentation.', + 'https://community.algolia.com/magento/doc/m1/indexing-queue/' + ); + } + + if ($this->isQueueFast()) { + $notices[] = $this->__('The average processing time of the queue has been performed under 3 minutes.'); + $notices[] = $this->__( + 'Adding more jobs in the Indexing Queue configuration would increase the indexing speed.', + $this->getUrl('adminhtml/system_config/edit/section/algoliasearch/') + ); + } + + return $notices; + } + + /** + * If the queue status is not "ready" and it is running for more than 5 minutes, we consider that the queue is stuck + * + * @return bool + */ + private function isQueueStuck() + { + if ($this->queueRunnerIndexer->getStatus() == Mage_Index_Model_Process::STATUS_PENDING) { + return false; + } + + if ($this->getTimeSinceLastIndexerUpdate() > self::CRON_QUEUE_FREQUENCY) { + return true; + } + + return false; + } + + /** + * Check if the queue indexer has not been processed for more than 1 hour + * + * @return bool + */ + private function isQueueNotProcessed() + { + return $this->getTimeSinceLastIndexerUpdate() > self::QUEUE_NOT_PROCESSED_LIMIT; + } + + /** + * Check if the average processing time of the queue is fast + * + * @return bool + */ + private function isQueueFast() + { + $averageProcessingTime = $this->queue->getAverageProcessingTime(); + + return !is_null($averageProcessingTime) && $averageProcessingTime < self::QUEUE_FAST_LIMIT; + } + + /** @return int */ + private function getIndexerLastUpdateTimestamp() + { + return $this->dateTime->gmtTimestamp($this->queueRunnerIndexer->getLatestUpdated()); + } + + /** @return int */ + private function getTimeSinceLastIndexerUpdate() + { + return $this->dateTime->gmtTimestamp('now') - $this->getIndexerLastUpdateTimestamp(); + } + + /** + * Prepare html output + * + * @return string + */ + protected function _toHtml() + { + if ($this->isQueueActive()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/Notifications.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/Notifications.php index 677c6dfb..21db5df4 100644 --- a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/Notifications.php +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/Notifications.php @@ -2,6 +2,8 @@ class Algolia_Algoliasearch_Block_Adminhtml_Notifications extends Mage_Adminhtml_Block_Template { + protected $_queueInfo; + public function getConfigurationUrl() { return $this->getUrl('adminhtml/system_config/edit/section/algoliasearch'); @@ -17,6 +19,10 @@ public function showNotification() public function getQueueInfo() { + if (isset($this->_queueInfo)) { + return $this->_queueInfo; + } + /** @var Algolia_Algoliasearch_Helper_Config $config */ $config = Mage::helper('algoliasearch/config'); @@ -26,17 +32,17 @@ public function getQueueInfo() $readConnection = $resource->getConnection('core_read'); - $size = (int) $readConnection->query('SELECT COUNT(*) as total_count FROM '.$tableName)->fetchColumn(0); + $size = (int)$readConnection->query('SELECT COUNT(*) as total_count FROM ' . $tableName)->fetchColumn(0); $maxJobsPerSingleRun = $config->getNumberOfJobToRun(); $etaMinutes = ceil($size / $maxJobsPerSingleRun) * 5; // 5 - assuming the queue runner runs every 5 minutes - $eta = $etaMinutes.' minutes'; + $eta = $etaMinutes . ' minutes'; if ($etaMinutes > 60) { $hours = floor($etaMinutes / 60); $restMinutes = $etaMinutes % 60; - $eta = $hours.' hours '.$restMinutes.' minutes'; + $eta = $hours . ' hours ' . $restMinutes . ' minutes'; } $queueInfo = array( @@ -45,6 +51,25 @@ public function getQueueInfo() 'eta' => $eta, ); - return $queueInfo; + $this->_queueInfo = $queueInfo; + + return $this->_queueInfo; + } + + /** + * Show notification based on condition + * + * @return bool + */ + protected function _toHtml() + { + $queueInfo = $this->getQueueInfo(); + if ($this->showNotification() + && $queueInfo['isEnabled'] === true + && $queueInfo['currentSize'] > 0) { + return parent::_toHtml(); + } + + return ''; } } diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit.php new file mode 100644 index 00000000..c8aafd35 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit.php @@ -0,0 +1,36 @@ +_objectId = 'sku'; + $this->_blockGroup = 'algoliasearch'; + $this->_controller = 'adminhtml_reindexsku'; + } + + /** + * Get header text. + * + * @return string + */ + public function getHeaderText() + { + return Mage::helper('algoliasearch')->__('Algolia Search - Reindex SKU(s)'); + } + + /** + * Set custom Algolia icon class. + * + * @return string + */ + public function getHeaderCssClass() + { + return 'icon-head algoliasearch-head-icon'; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit/Form.php b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit/Form.php new file mode 100644 index 00000000..1eed734c --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/Adminhtml/ReindexSku/Edit/Form.php @@ -0,0 +1,45 @@ + 'edit_form', + 'action' => $this->getUrl('*/*/reindexPost'), + 'method' => 'post', + )); + + $fieldset = $form->addFieldset('base_fieldset', array()); + + $html = '

'; + $html .= '

'.__('Enter here the SKU(s) you want to reindex separated by commas or carriage returns.').'

'; + $html .= '

'.__('You will be notified if there is any reason why your product can\'t be reindexed.').'

'; + $html .= '

'.__('It can be :').'

'; + $html .= ''; + $html .= '

'.__('You can reindex up to 10 SKUs at once.').'

'; + + $fieldset->addField('skus', 'textarea', array( + 'name' => 'skus', + 'label' => Mage::helper('algoliasearch')->__('Product SKU(s)'), + 'title' => Mage::helper('algoliasearch')->__('Product SKU(s)'), + 'required' => true, + 'style' => 'width:100%', + 'after_element_html' => $html, + )); + + $form->setUseContainer(true); + $this->setForm($form); + + return parent::_prepareForm(); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Block/System/Config/Form/Field/ProductAttributes.php b/app/code/community/Algolia/Algoliasearch/Block/System/Config/Form/Field/ProductAttributes.php new file mode 100644 index 00000000..d76e0427 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Block/System/Config/Form/Field/ProductAttributes.php @@ -0,0 +1,38 @@ +settings = array( + 'columns' => array( + 'attribute' => array( + 'label' => 'Attribute', + 'options' => function () { + $options = array(); + + /** @var Algolia_Algoliasearch_Helper_Entity_Producthelper $product_helper */ + $product_helper = Mage::helper('algoliasearch/entity_producthelper'); + $attributes = $product_helper->getAllAttributes(); + foreach ($attributes as $key => $label) { + $options[$key] = $key ?: $label; + } + + $options['custom_attribute'] = '[use custom attribute]'; + + return $options; + }, + 'rowMethod' => 'getAttribute', + 'width' => 150, + ), + ), + 'buttonLabel' => 'Add Attribute', + 'addAfter' => false, + ); + + parent::__construct(); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php b/app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php index bea47ea3..ee1fdf52 100644 --- a/app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php +++ b/app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php @@ -236,7 +236,13 @@ public function copySynonyms($fromIndexName, $toIndexName) $this->lastUsedIndexName = $toIndex; $this->lastTaskId = $res['taskID']; } - + + /** + * @param $fromIndexName + * @param $toIndexName + * + * @throws \AlgoliaSearch\AlgoliaException + */ public function copyQueryRules($fromIndexName, $toIndexName) { $fromIndex = $this->getIndex($fromIndexName); diff --git a/app/code/community/Algolia/Algoliasearch/Helper/Config.php b/app/code/community/Algolia/Algoliasearch/Helper/Config.php index e6bbee9b..90db0f1f 100644 --- a/app/code/community/Algolia/Algoliasearch/Helper/Config.php +++ b/app/code/community/Algolia/Algoliasearch/Helper/Config.php @@ -87,6 +87,7 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract const PREVENT_BACKEND_RENDERING = 'algoliasearch/advanced/prevent_backend_rendering'; const PREVENT_BACKEND_RENDERING_DISPLAY_MODE = 'algoliasearch/advanced/prevent_backend_rendering_display_mode'; const BACKEND_RENDERING_ALLOWED_USER_AGENTS = 'algoliasearch/advanced/backend_rendering_allowed_user_agents'; + const NON_CASTABLE_ATTRIBUTES = 'algoliasearch/advanced/non_castable_attributes'; const SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock'; const LOGGING_ENABLED = 'algoliasearch/credentials/debug'; @@ -716,6 +717,22 @@ public function getBackendRenderingDisplayMode($storeId = null) return Mage::getStoreConfig(self::PREVENT_BACKEND_RENDERING_DISPLAY_MODE, $storeId); } + public function getNonCastableAttributes($storeId = null) + { + $nonCastableAttributes = array(); + $config = unserialize(Mage::getStoreConfig(self::NON_CASTABLE_ATTRIBUTES, $storeId)); + + if (is_array($config)) { + foreach ($config as $attributeData) { + if (isset($attributeData['attribute'])) { + $nonCastableAttributes[] = $attributeData['attribute']; + } + } + } + + return $nonCastableAttributes; + } + private function getCustomRanking($configName, $storeId = null) { $attrs = unserialize(Mage::getStoreConfig($configName, $storeId)); diff --git a/app/code/community/Algolia/Algoliasearch/Helper/Data.php b/app/code/community/Algolia/Algoliasearch/Helper/Data.php index d06cbe58..d0518e4b 100644 --- a/app/code/community/Algolia/Algoliasearch/Helper/Data.php +++ b/app/code/community/Algolia/Algoliasearch/Helper/Data.php @@ -490,11 +490,9 @@ protected function getProductsRecords($storeId, $collection, $potentiallyDeleted continue; } - if ($product->isDeleted() === true - || $product->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_DISABLED - || $this->product_helper->shouldIndexProductByItsVisibility($product, $storeId) === false - || ($product->getStockItem()->is_in_stock == 0 && !$this->config->getShowOutOfStock($storeId)) - ) { + try { + $this->product_helper->canProductBeReindexed($product, $storeId); + } catch (Algolia_Algoliasearch_Model_Exception_ProductReindexException $e) { $productsToRemove[$productId] = $productId; continue; } @@ -511,7 +509,6 @@ protected function getProductsRecords($storeId, $collection, $potentiallyDeleted 'toRemove' => array_unique($productsToRemove), ); } - public function rebuildStoreProductIndexPage($storeId, $collectionDefault, $page, $pageSize, $emulationInfo = null, $productIds = null, $useTmpIndex = false) { if ($this->config->isEnabledBackend($storeId) === false) { diff --git a/app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php b/app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php index 65c3923e..d061ad2e 100644 --- a/app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php +++ b/app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php @@ -14,6 +14,9 @@ abstract class Algolia_Algoliasearch_Helper_Entity_Helper protected static $_activeCategories; protected static $_categoryNames; + /** @var array */ + private $nonCastableAttributes = array('sku', 'name', 'description'); + abstract protected function getIndexNameSuffix(); public function __construct() @@ -21,6 +24,12 @@ public function __construct() $this->config = Mage::helper('algoliasearch/config'); $this->algolia_helper = Mage::helper('algoliasearch/algoliahelper'); $this->logger = Mage::helper('algoliasearch/logger'); + + // Merge non castable attributes set in config + $this->nonCastableAttributes = array_merge( + $this->nonCastableAttributes, + $this->config->getNonCastableAttributes() + ); } public function getBaseIndexName($storeId = null) @@ -54,10 +63,8 @@ protected function try_cast($value) protected function castProductObject(&$productData) { - $nonCastableAttributes = array('sku', 'name', 'description'); - foreach ($productData as $key => &$data) { - if (in_array($key, $nonCastableAttributes, true) === true) { + if (in_array($key, $this->nonCastableAttributes, true) === true) { continue; } diff --git a/app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php b/app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php index e77042fd..37d4ddf5 100644 --- a/app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php +++ b/app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php @@ -1,7 +1,11 @@ algolia_helper->copyQueryRules($this->getIndexName($storeId), $this->getIndexName($storeId, $saveToTmpIndicesToo)); + $indexName = $this->getIndexName($storeId); + $indexNameTmp = $this->getIndexName($storeId, $saveToTmpIndicesToo); + + try { + $this->algolia_helper->copyQueryRules($indexName, $indexNameTmp); + } catch (AlgoliaException $e) { + // Fail silently if query rules are disabled on the app + // If QRs are disabled, nothing will happen and the extension will work as expected + if ($e->getMessage() !== 'Query Rules are not enabled on this application') { + throw $e; + } + } } } @@ -568,21 +583,29 @@ protected function handlePrice(Mage_Catalog_Model_Product &$product, $sub_produc $group_id = (int) $group->getData('customer_group_id'); if ($special_price && $special_price < $customData[$field][$currency_code]['group_'.$group_id]) { - $customData[$field][$currency_code]['group_'.$group_id.'_original_formated'] = $customData[$field][$currency_code]['default_formated']; + $customData[$field][$currency_code]['group_'.$group_id.'_original_formated'] = + $customData[$field][$currency_code]['default_formated']; $customData[$field][$currency_code]['group_'.$group_id] = $special_price; - $customData[$field][$currency_code]['group_'.$group_id.'_formated'] = $this->formatPrice($special_price, - false, $currency_code); + $customData[$field][$currency_code]['group_'.$group_id.'_formated'] = $this->formatPrice( + $special_price, + false, + $currency_code + ); } } - } else { - if ($special_price && $special_price < $customData[$field][$currency_code]['default']) { - $customData[$field][$currency_code]['default_original_formated'] = $customData[$field][$currency_code]['default_formated']; + } - $customData[$field][$currency_code]['default'] = $special_price; - $customData[$field][$currency_code]['default_formated'] = $this->formatPrice($special_price, - false, $currency_code); - } + if ($special_price && $special_price < $customData[$field][$currency_code]['default']) { + $customData[$field][$currency_code]['default_original_formated'] = + $customData[$field][$currency_code]['default_formated']; + + $customData[$field][$currency_code]['default'] = $special_price; + $customData[$field][$currency_code]['default_formated'] = $this->formatPrice( + $special_price, + false, + $currency_code + ); } if ($type == 'grouped' || $type == 'bundle' || $type == 'configurable') { @@ -598,15 +621,21 @@ protected function handlePrice(Mage_Catalog_Model_Product &$product, $sub_produc } else { if (count($sub_products) > 0) { foreach ($sub_products as $sub_product) { + if (!$sub_product->isSalable()) { + continue; + } + $price = (double) $taxHelper->getPrice($product, $sub_product->getFinalPrice(), $with_tax, null, null, null, $product->getStore(), null); $min = min($min, $price); $max = max($max, $price); } - } else { + } + + if ($min > $max) { $min = $max; - } // avoid to have PHP_INT_MAX in case of no subproducts (Corner case of visibility and stock options) + } // avoid to have PHP_INT_MAX in case of no salable subproducts (Corner case of visibility and stock options) } if ($min != $max) { @@ -1041,6 +1070,46 @@ public function getObject(Mage_Catalog_Model_Product $product) return $customData; } + /** + * Returns all parent product IDs, e.g. when simple product is part of configurable or bundle + * + * @param array $productIds + * @return array + */ + public function getParentProductIds(array $productIds) + { + $parentIds = array(); + foreach ($this->getCompositeTypes() as $typeInstance) { + $parentIds = array_merge($parentIds, $typeInstance->getParentIdsByChild($productIds)); + } + + return $parentIds; + } + + /** + * Returns composite product type instances + * + * @return Mage_Catalog_Model_Product_Type[] + * + * @see Mage_Catalog_Model_Resource_Product_Flat_Indexer::getProductTypeInstances() + */ + private function getCompositeTypes() + { + if ($this->compositeTypes === null) { + /** @var Mage_Catalog_Model_Product $productEmulator */ + $productEmulator = Mage::getModel('catalog/product'); + + /** @var Mage_Catalog_Model_Product_Type $productType */ + $productType = Mage::getModel('catalog/product_type'); + foreach ($productType->getCompositeTypes() as $typeId) { + $productEmulator->setTypeId($typeId); + $this->compositeTypes[$typeId] = $productType->factory($productEmulator); + } + } + + return $this->compositeTypes; + } + public function getAllProductIds($storeId) { $products = Mage::getModel('catalog/product')->getCollection(); @@ -1084,6 +1153,45 @@ private function getVisibilityAttributeValues($storeId) return $catalog_productVisibility->{$visibilityMethod}(); } + /** + * Check if product can be index on Algolia + * + * @param Mage_Catalog_Model_Product $product + * @param int $storeId + * + * @return bool + * + */ + public function canProductBeReindexed(Mage_Catalog_Model_Product $product, $storeId) + { + if ($product->isDeleted() === true) { + throw (new Algolia_Algoliasearch_Model_Exception_ProductDeletedException()) + ->withProduct($product) + ->withStoreId($storeId); + } + + if ($product->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_DISABLED) { + throw (new Algolia_Algoliasearch_Model_Exception_ProductDisabledException()) + ->withProduct($product) + ->withStoreId($storeId); + } + + if ($this->shouldIndexProductByItsVisibility($product, $storeId) === false) { + throw (new Algolia_Algoliasearch_Model_Exception_ProductNotVisibleException()) + ->withProduct($product) + ->withStoreId($storeId); + } + + if (!$this->config->getShowOutOfStock($storeId) + && !$product->getStockItem()->getIsInStock()) { + throw (new Algolia_Algoliasearch_Model_Exception_ProductOutOfStockException()) + ->withProduct($product) + ->withStoreId($storeId); + } + + return true; + } + private function explodeSynomyms($synonyms) { return array_map('trim', explode(',', $synonyms)); @@ -1178,22 +1286,28 @@ private function setFacetsQueryRules($indexName) private function clearFacetsQueryRules($index) { - $hitsPerPage = 100; - $page = 0; - do { - $fetchedQueryRules = $index->searchRules( - array( - 'context' => 'magento_filters', - 'page' => $page, - 'hitsPerPage' => $hitsPerPage, - ) - ); + try { + $hitsPerPage = 100; + $page = 0; + do { + $fetchedQueryRules = $index->searchRules(array( + 'context' => 'magento_filters', + 'page' => $page, + 'hitsPerPage' => $hitsPerPage, + )); + + foreach ($fetchedQueryRules['hits'] as $hit) { + $index->deleteRule($hit['objectID'], true); + } - foreach ($fetchedQueryRules['hits'] as $hit) { - $index->deleteRule($hit['objectID'], true); + $page++; + } while (($page * $hitsPerPage) < $fetchedQueryRules['nbHits']); + } catch (AlgoliaException $e) { + // Fail silently if query rules are disabled on the app + // If QRs are disabled, nothing will happen and the extension will work as expected + if ($e->getMessage() !== 'Query Rules are not enabled on this application') { + throw $e; } - - $page++; - } while (($page * $hitsPerPage) < $fetchedQueryRules['nbHits']); + } } } diff --git a/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductDeletedException.php b/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductDeletedException.php new file mode 100644 index 00000000..f33e05f3 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductDeletedException.php @@ -0,0 +1,5 @@ +product = $product; + + return $this; + } + + /** + * Add related store ID. + * + * @param int $storeId + * + * @return $this + */ + public function withStoreId($storeId) + { + $this->storeId = $storeId; + + return $this; + } + + /** + * Get related product. + * + * @return Mage_Catalog_Model_Product + */ + public function getProduct() + { + return $this->product; + } + + /** + * Get related store ID. + * + * @return int + */ + public function getStoreId() + { + return $this->storeId; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductUnknownSkuException.php b/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductUnknownSkuException.php new file mode 100644 index 00000000..2287f89b --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Exception/ProductUnknownSkuException.php @@ -0,0 +1,5 @@ +__('Algolia Search - Delete inactive products'); + return Mage::helper('algoliasearch')->__('Algolia Search - Remove inactive products from Algolia'); } public function getDescription() diff --git a/app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliaqueuerunner.php b/app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliaqueuerunner.php index 1c4ae6a3..0d13e32a 100644 --- a/app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliaqueuerunner.php +++ b/app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliaqueuerunner.php @@ -2,6 +2,7 @@ class Algolia_Algoliasearch_Model_Indexer_Algoliaqueuerunner extends Mage_Index_Model_Indexer_Abstract { + const INDEXER_ID = 'algolia_queue_runner'; const EVENT_MATCH_RESULT_KEY = 'algoliasearch_match_result'; /** @var Algolia_Algoliasearch_Helper_Config */ diff --git a/app/code/community/Algolia/Algoliasearch/Model/Job.php b/app/code/community/Algolia/Algoliasearch/Model/Job.php new file mode 100644 index 00000000..bd912812 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Job.php @@ -0,0 +1,60 @@ +_init('algoliasearch/job'); + } + + /** + * @return string + */ + public function getStatus() + { + $status = Algolia_Algoliasearch_Model_Source_JobStatuses::STATUS_PROCESSING; + + if (is_null($this->getPid())) { + $status = Algolia_Algoliasearch_Model_Source_JobStatuses::STATUS_NEW; + } + + if ((int) $this->getRetries() >= $this->getMaxRetries()) { + $status = Algolia_Algoliasearch_Model_Source_JobStatuses::STATUS_ERROR; + } + + return $status; + } + + /** + * @return string + */ + public function getStatusLabel() + { + $status = $this->getStatus(); + $labels = Mage::getModel('algoliasearch/source_jobStatuses')->getStatuses(); + + return isset($labels[$status]) ? $labels[$status] : $status; + } + + /** + * @param Exception $e + * + * @return Algolia_Algoliasearch_Model_Job + */ + public function saveError(Exception $e) + { + $this->setErrorLog($e->getMessage()); + $this->save($this); + + return $this; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Model/Observer.php b/app/code/community/Algolia/Algoliasearch/Model/Observer.php index 5013f903..48d06917 100644 --- a/app/code/community/Algolia/Algoliasearch/Model/Observer.php +++ b/app/code/community/Algolia/Algoliasearch/Model/Observer.php @@ -96,6 +96,10 @@ public function useAlgoliaSearchPopup(Varien_Event_Observer $observer) public function saveProduct(Varien_Event_Observer $observer) { + if ($this->isIndexerInManualMode('algolia_search_indexer')) { + return; + } + $product = $observer->getDataObject(); $product = Mage::getModel('catalog/product')->load($product->getId()); @@ -104,7 +108,9 @@ public function saveProduct(Varien_Event_Observer $observer) public function savePage(Varien_Event_Observer $observer) { - if (!$this->config->getApplicationID() || !$this->config->getAPIKey()) { + if (!$this->config->getApplicationID() + || !$this->config->getAPIKey() + || $this->isIndexerInManualMode('algolia_search_indexer_pages')) { return; } @@ -318,4 +324,15 @@ private function loadAnalyticsHandle(Varien_Event_Observer $observer) $observer->getData('layout')->getUpdate()->addHandle('algolia_search_handle_click_conversion_analytics'); } + + private function isIndexerInManualMode($indexerCode) + { + /** @var $process Mage_Index_Model_Process */ + $process = Mage::getModel('index/process')->load($indexerCode, 'indexer_code'); + if (!is_null($process) && $process->getMode() == Mage_Index_Model_Process::MODE_MANUAL) { + return true; + } + + return false; + } } diff --git a/app/code/community/Algolia/Algoliasearch/Model/Queue.php b/app/code/community/Algolia/Algoliasearch/Model/Queue.php index abe524fa..eccb3c0e 100644 --- a/app/code/community/Algolia/Algoliasearch/Model/Queue.php +++ b/app/code/community/Algolia/Algoliasearch/Model/Queue.php @@ -7,6 +7,7 @@ class Algolia_Algoliasearch_Model_Queue protected $table; protected $logTable; + protected $archiveTable; /** @var Magento_Db_Adapter_Pdo_Mysql */ protected $db; @@ -38,7 +39,8 @@ public function __construct() $coreResource = Mage::getSingleton('core/resource'); $this->table = $coreResource->getTableName('algoliasearch/queue'); - $this->logTable = $this->table.'_log'; + $this->logTable = $coreResource->getTableName('algoliasearch/queue_log'); + $this->archiveTable = $coreResource->getTableName('algoliasearch/queue_archive'); $this->db = $coreResource->getConnection('core_write'); @@ -61,6 +63,28 @@ public function add($class, $method, $data, $data_size) )); } + /** + * Return the average processing time for the 2 last two days + * (null if there was less than 100 runs with processed jobs) + * + * @throws \Zend_Db_Statement_Exception + * + * @return float|null + */ + public function getAverageProcessingTime() + { + $data = $this->db->query( + $this->db->select() + ->from($this->logTable, array('number_of_runs' => 'COUNT(duration)', 'average_time' => 'AVG(duration)')) + ->where('processed_jobs > 0 AND with_empty_queue = 0 AND started >= (CURDATE() - INTERVAL 2 DAY)') + ); + $result = $data->fetch(); + + return (int) $result['number_of_runs'] >= 100 && isset($result['average_time']) ? + (float) $result['average_time'] : + null; + } + public function runCron($nbJobs = null, $force = false) { if (!$this->config->isQueueActive() && $force === false) { @@ -122,24 +146,34 @@ public function run($maxJobs) $method = $job['method']; $model->{$method}(new Varien_Object($job['data'])); + // Delete one by one + $where = $this->db->quoteInto('job_id IN (?)', $job['merged_ids']); + $this->db->delete($this->table, $where); + $this->logRecord['processed_jobs'] += count($job['merged_ids']); } catch (\Exception $e) { $this->noOfFailedJobs++; + // Log error information + $logMessage = 'Queue processing ' . $job['pid'] . ' [KO]: + Class: ' . $job['class'] . ', + Method: ' . $job['method'] . ', + Parameters: ' . json_encode($job['data']); + $this->logger->log($logMessage); + + $logMessage = date('c') . ' ERROR: ' . get_class($e) . ': + ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . + "\nStack trace:\n" . $e->getTraceAsString(); + $this->logger->log($logMessage); + // Increment retries, set the job ID back to NULL - $updateQuery = "UPDATE {$this->db->quoteIdentifier($this->table, true)} SET pid = NULL, retries = retries + 1 WHERE job_id IN (".implode(', ', (array) $job['merged_ids']).")"; + $updateQuery = "UPDATE {$this->db->quoteIdentifier($this->table, true)} + SET pid = NULL, retries = retries + 1 , error_log = '" . addslashes($logMessage) . "' + WHERE job_id IN (".implode(', ', (array) $job['merged_ids']).")"; $this->db->query($updateQuery); - - // log error information - $this->logger->log("Queue processing {$job['pid']} [KO]: Mage::getSingleton({$job['class']})->{$job['method']}(".json_encode($job['data']).')'); - $this->logger->log(date('c').' ERROR: '.get_class($e).": '{$e->getMessage()}' in {$e->getFile()}:{$e->getLine()}\n"."Stack trace:\n".$e->getTraceAsString()); } } - // Delete only when finished to be able to debug the queue if needed - $where = $this->db->quoteInto('pid = ?', $pid); - $this->db->delete($this->table, $where); - $isFullReindex = ($maxJobs === -1); if ($isFullReindex) { $this->run(-1); @@ -148,14 +182,26 @@ public function run($maxJobs) } } + private function archiveFailedJobs($whereClause) + { + $this->db->query( + "INSERT INTO {$this->archiveTable} (pid, class, method, data, error_log, data_size, created_at) + SELECT pid, class, method, data, error_log, data_size, NOW() + FROM {$this->table} + WHERE " . $whereClause + ); + } + private function getJobs($maxJobs, $pid) { // Clear jobs with crossed max retries count $retryLimit = $this->config->getRetryLimit(); if ($retryLimit > 0) { $where = $this->db->quoteInto('retries >= ?', $retryLimit); + $this->archiveFailedJobs($where); $this->db->delete($this->table, $where); } else { + $this->archiveFailedJobs('retries > max_retries'); $this->db->delete($this->table, 'retries > max_retries'); } @@ -216,6 +262,15 @@ private function getJobs($maxJobs, $pid) } } + if (isset($firstJobId)) { + $lastJobId = $this->maxValueInArray($jobs, 'job_id'); + + // Reserve all new jobs since last run + $this->db->query("UPDATE {$this->db->quoteIdentifier($this->table, true)} + SET pid = " . $pid . ' + WHERE job_id >= ' . $firstJobId . " AND job_id <= $lastJobId"); + } + $this->db->commit(); } catch (\Exception $e) { $this->db->rollBack(); @@ -224,14 +279,6 @@ private function getJobs($maxJobs, $pid) throw $e; } - - if (isset($firstJobId)) { - $lastJobId = $this->maxValueInArray($jobs, 'job_id'); - - // Reserve all new jobs since last run - $this->db->query("UPDATE {$this->db->quoteIdentifier($this->table, true)} SET pid = ".$pid.' WHERE job_id >= '.$firstJobId." AND job_id <= $lastJobId"); - } - return $jobs; } @@ -416,4 +463,12 @@ private function clearOldLogRecords() $this->db->query("DELETE FROM {$this->logTable} WHERE id IN (" . implode(", ", $idsToDelete) . ")"); } } + + public function clearQueue($canClear = false) + { + if ($canClear) { + $this->db->truncateTable($this->table); + $this->logger->log("{$this->table} table has been truncated."); + } + } } diff --git a/app/code/community/Algolia/Algoliasearch/Model/Resource/Job.php b/app/code/community/Algolia/Algoliasearch/Model/Resource/Job.php new file mode 100644 index 00000000..3b098614 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Resource/Job.php @@ -0,0 +1,12 @@ +_init('algoliasearch/job', 'job_id'); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Model/Resource/Job/Collection.php b/app/code/community/Algolia/Algoliasearch/Model/Resource/Job/Collection.php new file mode 100644 index 00000000..457c600c --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Resource/Job/Collection.php @@ -0,0 +1,12 @@ +_init('algoliasearch/job'); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Model/Source/JobMethods.php b/app/code/community/Algolia/Algoliasearch/Model/Source/JobMethods.php new file mode 100644 index 00000000..01107a98 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Source/JobMethods.php @@ -0,0 +1,45 @@ + 'Save Settings', + 'saveConfigurationToAlgolia' => 'Save Configuration', + 'moveIndex' => 'Move Index', + 'moveProductsTmpIndex' => 'Move Products Temp Index', + 'deleteObjects' => 'Object Deletion', + 'rebuildStoreCategoryIndex' => 'Store Category Reindex', + 'rebuildCategoryIndex' => 'Category Reindex', + 'rebuildStoreProductIndex' => 'Store Product Reindex', + 'rebuildProductIndex' => 'Product Reindex', + 'rebuildStoreAdditionalSectionsIndex' => 'Store Additional Section Reindex', + 'rebuildAdditionalSectionsIndex' => 'Additional Section Reindex', + 'rebuildStoreSuggestionIndex' => 'Store Suggestion Reindex', + 'rebuildSuggestionIndex' => 'Sugesstion Reindex', + 'rebuildStorePageIndex' => 'Store Page Reindex', + 'rebuildPageIndex' => 'Page Reindex', + ); + + /** + * @return array + */ + public function getMethods() + { + return $this->_methods; + } + + /** + * @return array + */ + public function toOptionArray() + { + $options = array(); + foreach ($this->_methods as $method => $label) { + $option[] = array( + 'value' => $method, + 'label' => $label, + ); + } + return $options; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/Model/Source/JobStatuses.php b/app/code/community/Algolia/Algoliasearch/Model/Source/JobStatuses.php new file mode 100644 index 00000000..5a243843 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/Model/Source/JobStatuses.php @@ -0,0 +1,39 @@ + 'New', + self::STATUS_ERROR => 'Error', + self::STATUS_PROCESSING => 'Processing', + self::STATUS_COMPLETE => 'Complete' + ); + + /** + * @return array + */ + public function getStatuses() + { + return $this->_statuses; + } + + /** + * @return array + */ + public function toOptionArray() + { + $options = array(); + foreach ($this->_methods as $method => $label) { + $option[] = array( + 'value' => $method, + 'label' => $label, + ); + } + return $options; + } +} diff --git a/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/IndexingQueueController.php b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/IndexingQueueController.php new file mode 100644 index 00000000..03a724f3 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/IndexingQueueController.php @@ -0,0 +1,105 @@ +_checkQueueIsActivated(); + return parent::preDispatch(); + } + + public function indexAction() + { + $this->_title($this->__('System')) + ->_title($this->__('Algolia Search')) + ->_title($this->__('Indexing Queue')); + + $this->loadLayout(); + $this->_setActiveMenu('system/algolia/indexing_queue'); + $this->renderLayout(); + } + + public function viewAction() + { + $this->_title($this->__('System')) + ->_title($this->__('Algolia Search')) + ->_title($this->__('Indexing Queue')); + + $id = $this->getRequest()->getParam('id'); + if (!$id) { + Mage::getSingleton('adminhtml/session')->addError( + Mage::helper('algoliasearch')->__('Indexing Queue Job ID is not set.')); + $this->_redirect('*/*/'); + return; + } + + $job = Mage::getModel('algoliasearch/job')->load($id); + if (!$job->getId()) { + Mage::getSingleton('adminhtml/session')->addError( + Mage::helper('algoliasearch')->__('This indexing queue job no longer exists.')); + $this->_redirect('*/*/'); + return; + } + + Mage::register('algoliasearch_current_job', $job); + + $this->loadLayout(); + $this->_setActiveMenu('system/algolia/indexing_queue'); + $this->renderLayout(); + } + + public function clearAction() + { + try { + /** @var Algolia_Algoliasearch_Model_Queue $queue */ + $queue = Mage::getModel('algoliasearch/queue'); + $queue->clearQueue(true); + + Mage::getSingleton('adminhtml/session')->addSuccess('Indexing Queue has been cleared.'); + } catch (Exception $e) { + Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); + } + + $this->_redirect('*/*/'); + } + + public function resetAction() + { + try { + $queueRunnerIndexer = Mage::getModel('index/indexer') + ->getProcessByCode(Algolia_Algoliasearch_Model_Indexer_Algoliaqueuerunner::INDEXER_ID); + $queueRunnerIndexer->setStatus(Mage_Index_Model_Process::STATUS_PENDING); + $queueRunnerIndexer->save(); + + Mage::getSingleton('adminhtml/session')->addSuccess('Indexing Queue has been reset.'); + } catch (Exception $e) { + Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); + } + + $this->_redirect('*/*/'); + } + + protected function _checkQueueIsActivated() + { + if (!Mage::helper('algoliasearch/config')->isQueueActive()) { + Mage::getSingleton('adminhtml/session')->addWarning( + $this->__('The indexing queue is not enabled. Please activate it in your Algolia configuration.', + $this->getUrl('adminhtml/system_config/edit/section/algoliasearch'))); + } + } + + /** + * Check ACL permissions. + * + * @return bool + */ + protected function _isAllowed() + { + return Mage::getSingleton('admin/session')->isAllowed('system/algoliasearch/indexing_queue'); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/AlgoliaQueueController.php b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/QueueController.php similarity index 80% rename from app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/AlgoliaQueueController.php rename to app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/QueueController.php index ced6b9fc..704fd054 100644 --- a/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/AlgoliaQueueController.php +++ b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/QueueController.php @@ -1,6 +1,6 @@ getTableName('algoliasearch/queue'); - try { - $writeConnection = $resource->getConnection('core_write'); - $writeConnection->query('TRUNCATE TABLE '.$tableName); + /** @var Algolia_Algoliasearch_Model_Queue $queue */ + $queue = Mage::getModel('algoliasearch/queue'); + $queue->clearQueue(true); $status = array('status' => 'ok'); } catch (\Exception $e) { diff --git a/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/ReindexSkuController.php b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/ReindexSkuController.php new file mode 100644 index 00000000..56334943 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/controllers/Adminhtml/Algoliasearch/ReindexSkuController.php @@ -0,0 +1,132 @@ +_title($this->__('System')) + ->_title($this->__('Algolia Search')) + ->_title($this->__('Reindex SKU(s)')); + + $this->loadLayout(); + $this->_setActiveMenu('system/algolia/reindexsku'); + $this->renderLayout(); + } + + public function reindexPostAction() + { + if ($this->getRequest()->getParam('skus')) { + $skus = array_filter(array_map('trim', preg_split("/(,|\r\n|\n|\r)/", $this->getRequest()->getParam('skus')))); + $session = Mage::getSingleton('adminhtml/session'); + $stores = Mage::app()->getStores(); + $config = Mage::helper('algoliasearch/config'); + + foreach ($stores as $storeId => $store) { + if ($config->isEnabledBackend($storeId) === false) { + unset($stores[$storeId]); + } + } + + if (count($skus) > self::MAX_SKUS) { + $session->addError($this->__('The maximal number of SKU(s) is %s. Could you please remove some SKU(s) to fit into the limit?', + self::MAX_SKUS)); + $this->_redirect('*/*/'); + + return; + } + + // Load the collection instead of loading every one individually + $collection = Mage::getResourceModel('catalog/product_collection') + ->addAttributeToSelect('*') + ->addAttributeToFilter('sku', array('in' => $skus)) + ->setFlag('require_stock_items', true); + + foreach ($skus as $sku) { + try { + $product = $collection->getItemByColumnValue('sku', $sku); + if (!$product) { + throw new Algolia_Algoliasearch_Model_Exception_ProductUnknownSkuException($this->__('Product with SKU "%s" was not found.', $sku)); + } + $this->checkAndReindex($product, $stores); + } catch (Algolia_Algoliasearch_Model_Exception_ProductUnknownSkuException $e) { + $session->addError($e->getMessage()); + } catch (Algolia_Algoliasearch_Model_Exception_ProductDisabledException $e) { + $session->addError( + $this->__('The product "%s" (%s) is disabled in store "%s".', $e->getProduct()->getName(), $e->getProduct()->getSku(), $stores[$e->getStoreId()]->getName()) + ); + } catch (Algolia_Algoliasearch_Model_Exception_ProductDeletedException $e) { + $session->addError( + $this->__('The product "%s" (%s) is deleted from store "%s".', $e->getProduct()->getName(), $e->getProduct()->getSku(), $stores[$e->getStoreId()]->getName()) + ); + } catch (Algolia_Algoliasearch_Model_Exception_ProductNotVisibleException $e) { + $session->addError( + $this->__('The product "%s" (%s) is not visible in store "%s".', $e->getProduct()->getName(), $e->getProduct()->getSku(), $stores[$e->getStoreId()]->getName()) + ); + } catch (Algolia_Algoliasearch_Model_Exception_ProductOutOfStockException $e) { + $session->addError( + $this->__('The product "%s" (%s) is out of stock in store "%s".', $e->getProduct()->getName(), $e->getProduct()->getSku(), $stores[$e->getStoreId()]->getName()) + ); + } catch (Exception $e) { + $session->addError($e->getMessage()); + } + } + } + + $this->_redirect('*/*/'); + } + + /** + * @param Mage_Catalog_Model_Product $product + * @param array $stores + */ + protected function checkAndReindex($product, array $stores) + { + /** @var Algolia_Algoliasearch_Helper_Entity_Producthelper $productHelper */ + $productHelper = Mage::helper('algoliasearch/entity_producthelper'); + $session = Mage::getSingleton('adminhtml/session'); + + foreach ($stores as $storeId => $store) { + if (!in_array($storeId, $product->getStoreIds())) { + $session->addNotice($this->__('The product "%s" (%s) is not associated with store "%s".', + $product->getName(), $product->getSku(), $store->getName())); + continue; + } + try { + $productHelper->canProductBeReindexed($product, $storeId); + } catch (Algolia_AlgoliaSearch_Model_Exception_ProductNotVisibleException $e) { + // If it's a simple product that is not visible, try to index its parent if it exists + if ($e->getProduct()->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_SIMPLE) { + $parentId = $productHelper->getParentProductIds(array($e->getProduct()->getId())); + if (isset($parentId[0])) { + $parentProduct = Mage::getModel('catalog/product')->load($parentId[0]); + $session->addError( + $this->__('The product "%s" (%s) is not visible but it has a parent product "%s" (%s) for store "%s".', + $e->getProduct()->getName(), $e->getProduct()->getSku(), $parentProduct->getName(), + $parentProduct->getSku(), $stores[$e->getStoreId()]->getName())); + $this->checkAndReindex($parentProduct, array($stores[$e->getStoreId()])); + continue; + } + } + } + $productIds = array($product->getId()); + $productIds = array_merge($productIds, $productHelper->getParentProductIds($productIds)); + + Mage::helper('algoliasearch')->rebuildStoreProductIndex($storeId, $productIds); + + $session->addSuccess($this->__('The product "%s" (%s) has been reindexed for store "%s".', + $product->getName(), $product->getSku(), $store->getName())); + } + } + + /** + * Check ACL permissions. + * + * @return bool + */ + protected function _isAllowed() + { + return Mage::getSingleton('admin/session')->isAllowed('system/algoliasearch/reindexsku'); + } +} diff --git a/app/code/community/Algolia/Algoliasearch/etc/adminhtml.xml b/app/code/community/Algolia/Algoliasearch/etc/adminhtml.xml index 45ca42e9..feee0492 100644 --- a/app/code/community/Algolia/Algoliasearch/etc/adminhtml.xml +++ b/app/code/community/Algolia/Algoliasearch/etc/adminhtml.xml @@ -1,5 +1,32 @@ + + + + + Algolia Search + 93 + + + Configurations + adminhtml/system_config/edit/section/algoliasearch + 0 + + + Indexing Queue + adminhtml/algoliasearch_indexingqueue + 10 + + + Reindex SKU(s) + adminhtml/algoliasearch_reindexsku + 20 + + + + + + @@ -14,6 +41,19 @@ + + Algolia Search + + + Indexing Queue + 100 + + + Reindex SKU(s) + 110 + + + diff --git a/app/code/community/Algolia/Algoliasearch/etc/config.xml b/app/code/community/Algolia/Algoliasearch/etc/config.xml index 9ba85269..304c49e7 100644 --- a/app/code/community/Algolia/Algoliasearch/etc/config.xml +++ b/app/code/community/Algolia/Algoliasearch/etc/config.xml @@ -2,7 +2,7 @@ - 1.14.1 + 1.15.0 @@ -96,6 +96,15 @@ algoliasearch_queue
+ + algoliasearch_queue
+
+ + algoliasearch_queue_log
+
+ + algoliasearch_queue_archive
+
diff --git a/app/code/community/Algolia/Algoliasearch/etc/system.xml b/app/code/community/Algolia/Algoliasearch/etc/system.xml index 37a4ba1f..a2d1045f 100644 --- a/app/code/community/Algolia/Algoliasearch/etc/system.xml +++ b/app/code/community/Algolia/Algoliasearch/etc/system.xml @@ -1310,6 +1310,20 @@ 1 + + + algoliasearch/system_config_form_field_productAttributes + algoliasearch/system_config_backend_serialized_array + 120 + 1 + 1 + 1 + + + + diff --git a/app/code/community/Algolia/Algoliasearch/sql/algoliasearch_setup/mysql4-upgrade-1.14.1-1.15.0.php b/app/code/community/Algolia/Algoliasearch/sql/algoliasearch_setup/mysql4-upgrade-1.14.1-1.15.0.php new file mode 100644 index 00000000..d278b403 --- /dev/null +++ b/app/code/community/Algolia/Algoliasearch/sql/algoliasearch_setup/mysql4-upgrade-1.14.1-1.15.0.php @@ -0,0 +1,20 @@ +startSetup(); + +$tableName = $installer->getTable('algoliasearch/queue_archive'); + +$installer->run(" +CREATE TABLE IF NOT EXISTS `{$tableName}` ( + `pid` int(11) DEFAULT NULL COMMENT 'Pid', + `class` varchar(50) NOT NULL COMMENT 'Class', + `method` varchar(50) NOT NULL COMMENT 'Method', + `data` text NOT NULL COMMENT 'Data', + `error_log` text NOT NULL COMMENT 'Error Log', + `data_size` int(11) DEFAULT NULL COMMENT 'Data Size', + `created_at` datetime NOT NULL COMMENT 'Created At' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +"); + +$installer->endSetup(); diff --git a/app/design/adminhtml/default/default/layout/algoliasearch.xml b/app/design/adminhtml/default/default/layout/algoliasearch.xml index 9397aee3..5ef89b17 100644 --- a/app/design/adminhtml/default/default/layout/algoliasearch.xml +++ b/app/design/adminhtml/default/default/layout/algoliasearch.xml @@ -20,4 +20,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/design/adminhtml/default/default/template/algoliasearch/notifications.phtml b/app/design/adminhtml/default/default/template/algoliasearch/notifications.phtml index 3a432862..4451146c 100644 --- a/app/design/adminhtml/default/default/template/algoliasearch/notifications.phtml +++ b/app/design/adminhtml/default/default/template/algoliasearch/notifications.phtml @@ -1,17 +1,6 @@ showNotification() === false) { - return; -} - $queueInfo = $this->getQueueInfo(); - -if ($queueInfo['isEnabled'] === true && $queueInfo['currentSize'] === 0) { - return; -} - ?>
diff --git a/app/design/adminhtml/default/default/template/algoliasearch/queue/status.phtml b/app/design/adminhtml/default/default/template/algoliasearch/queue/status.phtml new file mode 100644 index 00000000..e29dc4c9 --- /dev/null +++ b/app/design/adminhtml/default/default/template/algoliasearch/queue/status.phtml @@ -0,0 +1,15 @@ + +
+
+

+ __('Status of the queue: %s', $this->getQueueRunnerStatus()); ?>
+ getLastQueueUpdate()): ?> + __('Last update: %s', $this->getLastQueueUpdate()); ?> + +

+ getNotices() ?> + +

+ +
+
\ No newline at end of file diff --git a/app/design/frontend/base/default/template/algoliasearch/autocomplete/product.phtml b/app/design/frontend/base/default/template/algoliasearch/autocomplete/product.phtml index af59601b..5ddaef4a 100644 --- a/app/design/frontend/base/default/template/algoliasearch/autocomplete/product.phtml +++ b/app/design/frontend/base/default/template/algoliasearch/autocomplete/product.phtml @@ -11,7 +11,7 @@ $storeId = Mage::app()->getStore()->getStoreId(); $currencyCode = Mage::app()->getStore()->getCurrentCurrencyCode(); $priceKey = '.'.$currencyCode.'.default'; -if ($config->isCustomerGroupsEnabled($storeId) && $customerGroupId !== 0) { +if ($config->isCustomerGroupsEnabled($storeId)) { $priceKey = '.'.$currencyCode.'.group_'.$customerGroupId; } diff --git a/app/design/frontend/base/default/template/algoliasearch/instantsearch/hit-item.phtml b/app/design/frontend/base/default/template/algoliasearch/instantsearch/hit-item.phtml index ba11267b..a3bedf8f 100644 --- a/app/design/frontend/base/default/template/algoliasearch/instantsearch/hit-item.phtml +++ b/app/design/frontend/base/default/template/algoliasearch/instantsearch/hit-item.phtml @@ -11,7 +11,7 @@ $storeId = Mage::app()->getStore()->getStoreId(); $currencyCode = Mage::app()->getStore()->getCurrentCurrencyCode(); $priceKey = '.'.$currencyCode.'.default'; -if ($config->isCustomerGroupsEnabled($storeId) && $customerGroupId !== 0) { +if ($config->isCustomerGroupsEnabled($storeId)) { $priceKey = '.'.$currencyCode.'.group_'.$customerGroupId; } diff --git a/app/design/frontend/base/default/template/algoliasearch/internals/configuration.phtml b/app/design/frontend/base/default/template/algoliasearch/internals/configuration.phtml index 8946509c..add7d9b1 100644 --- a/app/design/frontend/base/default/template/algoliasearch/internals/configuration.phtml +++ b/app/design/frontend/base/default/template/algoliasearch/internals/configuration.phtml @@ -259,6 +259,7 @@ $algoliaJsConfig = array( 'priceKey' => $priceKey, 'currencyCode' => $currencyCode, 'currencySymbol' => $currencySymbol, + 'priceFormat' => Mage::app()->getLocale()->getJsPriceFormat(), 'maxValuesPerFacet' => (int) $config->getMaxValuesPerFacet(), 'autofocus' => true, 'ccAnalytics' => array( diff --git a/js/algoliasearch/instantsearch.js b/js/algoliasearch/instantsearch.js index 8357d97d..ab146457 100644 --- a/js/algoliasearch/instantsearch.js +++ b/js/algoliasearch/instantsearch.js @@ -461,7 +461,9 @@ document.addEventListener("DOMContentLoaded", function (event) { }, tooltips: { format: function (formattedValue) { - return parseInt(formattedValue); + return facet.attribute.match(/price/) === null ? + parseInt(formattedValue) : + formatCurrency(formattedValue, algoliaConfig.priceFormat); } } }]; diff --git a/js/algoliasearch/internals/adminhtml/admin_scripts.js b/js/algoliasearch/internals/adminhtml/admin_scripts.js index 37846fcf..bbe45452 100644 --- a/js/algoliasearch/internals/adminhtml/admin_scripts.js +++ b/js/algoliasearch/internals/adminhtml/admin_scripts.js @@ -40,7 +40,7 @@ algoliaAdminBundle.$(function($) { position = url.indexOf('/system_config/edit/'), baseUrl = url.substring(0, position); - $.getJSON(baseUrl + '/algoliaqueue', function(queueInfo) { + $.getJSON(baseUrl + '/algoliasearch_queue', function(queueInfo) { var message = ' ' + 'Indexing queue is not enabled
' + 'It\'s highly recommended to enable it, especially if you are on production environment. ' + @@ -80,7 +80,7 @@ algoliaAdminBundle.$(function($) { } }, 200); - $.getJSON(baseUrl + '/algoliaqueue/truncate', function(payload) { + $.getJSON(baseUrl + '/algoliasearch_queue/truncate', function(payload) { window.clearInterval(dots); if (payload.status === 'ok') { diff --git a/js/algoliasearch/internals/frontend/common.js b/js/algoliasearch/internals/frontend/common.js index b64c4458..d380e074 100644 --- a/js/algoliasearch/internals/frontend/common.js +++ b/js/algoliasearch/internals/frontend/common.js @@ -279,7 +279,7 @@ document.addEventListener("DOMContentLoaded", function (e) { displayKey: 'query', name: section.name, templates: { - suggestion: function (hit) { + suggestion: function (hit, payload) { if (hit.facet) { hit.category = hit.facet.value; } @@ -321,13 +321,17 @@ document.addEventListener("DOMContentLoaded", function (e) { if (section.name === 'products') { source.templates.footer = function (query, content) { var keys = []; - for (var key in content.facets['categories.level0']) { - var url = algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + encodeURIComponent(query.query) + '#q=' + encodeURIComponent(query.query) + '&hFR[categories.level0][0]=' + encodeURIComponent(key) + '&idx=' + algoliaConfig.indexName + '_products'; - keys.push({ - key: key, - value: content.facets['categories.level0'][key], - url: url - }); + for (var i = 0; i 0) { - ors += '' + keys[0].key + ''; - } - - if (keys.length > 1) { - ors += ', ' + keys[1].key + ''; + var orsTab = []; + for (var i = 0; i < keys.length && i < 2; i++) { + orsTab.push('' + keys[i].key + ''); + } + ors = orsTab.join(', '); } var allUrl = algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + encodeURIComponent(query.query); diff --git a/skin/adminhtml/base/default/algoliasearch/algoliasearch.css b/skin/adminhtml/base/default/algoliasearch/algoliasearch.css index 2d7145ea..ae2d0448 100644 --- a/skin/adminhtml/base/default/algoliasearch/algoliasearch.css +++ b/skin/adminhtml/base/default/algoliasearch/algoliasearch.css @@ -3,3 +3,18 @@ background-size: 16px 16px; border-color: #bee1ee; } + +.algoliasearch-head-icon { + background: url(/skin/frontend/base/default/algoliasearch/algolia-admin-menu.svg) no-repeat; + background-size: 16px 16px; +} + +.algolia-notice { + border: 1px solid #dfdfdf; + background-position: 10px 10px; + padding-left: 35px; + padding-top: 10px; + margin-bottom: 1rem; + color: #333; + font-size: 13px; +} \ No newline at end of file diff --git a/skin/frontend/base/default/algoliasearch/algoliasearch.css b/skin/frontend/base/default/algoliasearch/algoliasearch.css index b3a80488..0d8259c8 100644 --- a/skin/frontend/base/default/algoliasearch/algoliasearch.css +++ b/skin/frontend/base/default/algoliasearch/algoliasearch.css @@ -318,6 +318,7 @@ a.ais-current-refined-values--link:hover position: absolute; background: #FFFFFF; top: -2em; + left: -50%; min-width: 20px; text-align: center; font-size: .8em;