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 .= ''.__('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 @@