From 4e5a5c826c4125acc532bb7a92762a9109dc632a Mon Sep 17 00:00:00 2001
From: Jonathan Jordan <jojonaloha@1579186.no-reply.drupal.org>
Date: Tue, 23 May 2017 17:43:09 -0700
Subject: [PATCH] Issue #2508431 by jojonaloha: An entity queue for each term?

---
 .../views/entityqueue_handler_sort_position.inc    |  24 +++
 .../SmartQueueEntitiesQueueHandler.class.php       | 238 +++++++++++++++++++++
 .../entityqueue/handler/smartqueue_entities.inc    |  12 ++
 .../smartqueue_entities/smartqueue_entities.info   |   9 +
 .../smartqueue_entities.install                    |  13 ++
 .../smartqueue_entities/smartqueue_entities.module | 148 +++++++++++++
 ...ueue_entities_handler_relationship_subqueue.inc | 154 +++++++++++++
 7 files changed, 598 insertions(+)
 create mode 100644 modules/smartqueue_entities/plugins/entityqueue/handler/SmartQueueEntitiesQueueHandler.class.php
 create mode 100644 modules/smartqueue_entities/plugins/entityqueue/handler/smartqueue_entities.inc
 create mode 100644 modules/smartqueue_entities/smartqueue_entities.info
 create mode 100644 modules/smartqueue_entities/smartqueue_entities.install
 create mode 100644 modules/smartqueue_entities/smartqueue_entities.module
 create mode 100644 modules/smartqueue_entities/smartqueue_entities_handler_relationship_subqueue.inc

diff --git a/includes/views/entityqueue_handler_sort_position.inc b/includes/views/entityqueue_handler_sort_position.inc
index ea27f08..634943c 100644
--- a/includes/views/entityqueue_handler_sort_position.inc
+++ b/includes/views/entityqueue_handler_sort_position.inc
@@ -12,6 +12,28 @@
  */
 class entityqueue_handler_sort_position extends views_handler_sort {
 
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['null_order'] = array('default' => 'DESC');
+
+    return $options;
+  }
+
+  function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    $options = $this->sort_options();
+    if (!empty($options)) {
+      $form['null_order'] = array(
+        '#type' => 'select',
+        '#title' => t('Null order'),
+        '#options' => $options,
+        '#default_value' => $this->options['null_order'],
+      );
+    }
+  }
+
   /**
    * Called to add the sort to a query.
    */
@@ -29,6 +51,8 @@ class entityqueue_handler_sort_position extends views_handler_sort {
     }
     // Add the field.
     if ($field_table_alias) {
+      $alias = $field_table_alias . '_isnull';
+      $this->query->add_orderby(NULL, "$field_table_alias.$this->real_field IS NULL", $this->options['null_order'], $alias);
       $this->query->add_orderby($field_table_alias, $this->real_field, $this->options['order']);
     }
     else {
diff --git a/modules/smartqueue_entities/plugins/entityqueue/handler/SmartQueueEntitiesQueueHandler.class.php b/modules/smartqueue_entities/plugins/entityqueue/handler/SmartQueueEntitiesQueueHandler.class.php
new file mode 100644
index 0000000..77594c7
--- /dev/null
+++ b/modules/smartqueue_entities/plugins/entityqueue/handler/SmartQueueEntitiesQueueHandler.class.php
@@ -0,0 +1,238 @@
+<?php
+
+class SmartQueueEntitiesQueueHandler extends EntityQueueHandlerBase {
+
+  protected $isBuildingSubqueues = FALSE;
+
+  /**
+   * Overrides EntityQueueHandlerBase::settingsForm().
+   */
+  public function settingsForm(&$form_state) {
+    $form = array(
+      'warning' => array(
+        '#prefix' => '<div class="messages warning">',
+        '#suffix' => '</div>',
+        '#markup' => t('These settings cannot be changed once the queue is created.'),
+      ),
+    );
+
+    $handler_settings = array();
+    // Load handler settings from form state
+    // @todo make this so the implementing methods do not need to know the underlying form structure?
+    if (isset($form_state['input']['handler_settings']['smartqueue_entities'])) {
+      // $form_state['input'] is used because that is what is available during and #ajax callback
+      $handler_settings = $form_state['input']['handler_settings']['smartqueue_entities'];
+    }
+    else if (isset($this->queue->settings['handler_settings'])) {
+      $handler_settings = $this->queue->settings['handler_settings'];
+    }
+
+    $source_type = isset($handler_settings['entity_type']) ? $handler_settings['entity_type'] : NULL;
+
+    // Get valid target entity types.
+    $entity_info = entity_get_info();
+    unset($entity_info['entityqueue_subqueue']);
+
+    $entity_types = array();
+    foreach ($entity_info as $entity_type => $info) {
+      $entity_types[$entity_type] = $info['label'];
+    }
+    $form['entity_type'] = array(
+      '#type' => 'select',
+      '#title' => t('Subqueues entity type'),
+      '#description' => t('The entity type to use to create subqueues from.'),
+      '#options' => $entity_types,
+      '#default_value' => $source_type,
+      '#required' => TRUE,
+      '#disabled' => $this->queue->is_new ? FALSE : TRUE,
+      '#ajax' => array(
+        'callback' => 'smartqueue_entities_entity_type_callback',
+        'wrapper' => 'smartqueue-entities-source-bundles',
+      ),
+    );
+
+    $source_bundles = !empty($handler_settings['source_bundles']) ? $handler_settings['source_bundles'] : array();
+    $bundles = array();
+    foreach ($entity_info[$source_type]['bundles'] as $bundle_name => $bundle_info) {
+      $bundles[$bundle_name] = $bundle_info['label'];
+    }
+    $form['source_bundles'] = array(
+      '#type' => 'checkboxes',
+      '#title' => $source_type == 'node' ? t('Content types') : t('Bundles'),
+      '#prefix' => '<div id="smartqueue-entities-source-bundles">',
+      '#suffix' => '</div>',
+      '#options' => $bundles,
+      '#default_value' => $source_bundles,
+      '#multiple' => TRUE,
+      '#required' => TRUE,
+      '#disabled' => $this->queue->is_new ? FALSE : TRUE,
+      '#description' => t('The bundles of the entity type that will be used to create subqueues.'),
+      '#element_validate' => array('smartqueue_entities_element_validate_source_bundles'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function canDeleteSubqueue(EntitySubqueue $subqueue) {
+    return FALSE;
+  }
+
+  /**
+   * Loads the EntitySubqueue related to the given entity.
+   *
+   * @param $entity
+   *
+   * @return EntitySubqueue || FALSE
+   */
+  function loadSubqueueForEntity($entity) {
+    $entity_type = $this->queue->settings['handler_settings']['entity_type'];
+    list($entity_id, ) = entity_extract_ids($entity_type, $entity);
+    $subqueue_name = $this->generateSubqueueName($entity_id);
+
+    return entityqueue_subqueue_load($subqueue_name);
+  }
+
+  /**
+   * Creates an EntitySubqueue related to the given entity without saving it to the database.
+   *
+   * @param $entity
+   *
+   * @return EntitySubqueue
+   */
+  public function createSubqueueForEntity($entity) {
+    global $user;
+
+    $entity_type = $this->queue->settings['handler_settings']['entity_type'];
+    list($entity_id, ) = entity_extract_ids($entity_type, $entity);
+
+    $subqueue = entityqueue_subqueue_create(array(
+      'queue' => $this->queue->name,
+      'name' => $this->generateSubqueueName($entity_id),
+      'module' => 'smartqueue_entities',
+      'uid' => $user->uid,
+    ));
+    $subqueue->label = $this->getSubqueueLabel($subqueue);
+
+    return $subqueue;
+  }
+
+  public function getSubqueueLabel(EntitySubqueue $subqueue) {
+    $entity_id = str_replace($this->queue->name . '_', '', $subqueue->name);
+    $entity_type = $this->queue->settings['handler_settings']['entity_type'];
+    if ($entity_type && $entity_id) {
+      $entity = entity_load_single($entity_type, $entity_id);
+      return entity_label($entity_type, $entity);
+    }
+    return t('Missing subqueue source entity');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadFromCode() {
+    $this->ensureSubqueues();
+    $this->updateSmartqueueList();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function insert() {
+    $this->ensureSubqueues();
+    $this->updateSmartqueueList();
+  }
+
+  public function postDelete() {
+    parent::postDelete();
+
+    $handler_settings = $this->queue->settings['handler_settings'];
+    $entity_type = $handler_settings['entity_type'];
+    $queues = variable_get('smartqueue_entities_by_source', array());
+    unset($queues[$entity_type][$this->queue->name]);
+    variable_set('smartqueue_entities_by_source', $queues);
+  }
+
+  /**
+   * Makes sure that a subqueue exists for each source entity.
+   */
+  protected function ensureSubqueues() {
+    global $user;
+
+    // Avoid a loop that causes inserts to fail due to duplicate keys
+    if ($this->isBuildingSubqueues) {
+      return;
+    }
+    $this->isBuildingSubqueues = TRUE;
+
+    $handler_settings = $this->queue->settings['handler_settings'];
+
+    $source_type = $handler_settings['entity_type'];
+    $source_bundles = $handler_settings['source_bundles'];
+
+    if (empty($source_type) || empty($source_bundles)) {
+      watchdog('error', 'Smartqueue @name is missing required entity type and source bundle configuration', array(
+        '@name' => $this->queue->label(),
+      ));
+      return FALSE;
+    }
+
+    $transaction = db_transaction();
+
+    // @todo add a count and range, use a batch process if there are a lot of entities
+    $efq = new EntityFieldQuery();
+    $result = $efq->entityCondition('entity_type', $source_type)
+      ->entityCondition('bundle', $source_bundles)
+      ->execute();
+    if (isset($result[$source_type])) {
+      $entity_ids = array_keys($result[$source_type]);
+      $subqueue_names = array_map(array($this, 'generateSubqueueName'), $entity_ids);
+    }
+
+    if (!empty($subqueue_names)) {
+      // Check subqueues exist for each entity id
+      $existing_subqueues = db_select('entityqueue_subqueue', 'sq')
+        ->fields('sq', array('name'))
+        ->condition('sq.queue', $this->queue->name)
+        ->condition('sq.name', $subqueue_names)
+        ->execute()
+        ->fetchCol();
+      $missing_subqueues = array_diff($subqueue_names, $existing_subqueues);
+      foreach ($missing_subqueues as $missing_subqueue) {
+        $subqueue = entityqueue_subqueue_create(array(
+          'queue' => $this->queue->name,
+          'name' => $missing_subqueue,
+          'module' => 'smartqueue_entities',
+          'uid' => $user->uid,
+        ));
+        $subqueue->label = $this->getSubqueueLabel($subqueue);
+
+        entity_get_controller('entityqueue_subqueue')->save($subqueue, $transaction);
+      }
+    }
+
+    $this->isBuildingSubqueues = FALSE;
+  }
+
+  protected function generateSubqueueName($entity_id) {
+    return $this->queue->name . '_' . $entity_id;
+  }
+
+  protected function updateSmartqueueList() {
+    // Set a variable to quickly fetch smartqueues by the source entity type.
+    $handler_settings = $this->queue->settings['handler_settings'];
+    $entity_type = $handler_settings['entity_type'];
+    $source_bundles = $handler_settings['source_bundles'];
+    $queues = variable_get('smartqueue_entities_by_source', array());
+    if (!isset($queues[$entity_type])) {
+      $queues[$entity_type] = array();
+    }
+    $queues[$entity_type][$this->queue->name] = $source_bundles;
+    variable_set('smartqueue_entities_by_source', $queues);
+  }
+
+
+
+}
\ No newline at end of file
diff --git a/modules/smartqueue_entities/plugins/entityqueue/handler/smartqueue_entities.inc b/modules/smartqueue_entities/plugins/entityqueue/handler/smartqueue_entities.inc
new file mode 100644
index 0000000..d7172aa
--- /dev/null
+++ b/modules/smartqueue_entities/plugins/entityqueue/handler/smartqueue_entities.inc
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Entityqueue handler for multiple subqueues.
+ */
+
+$plugin = array(
+  'title' => t('Smartqueue entities subqueues'),
+  'class' => 'SmartQueueEntitiesQueueHandler',
+  'queue type' => 'multiple',
+);
diff --git a/modules/smartqueue_entities/smartqueue_entities.info b/modules/smartqueue_entities/smartqueue_entities.info
new file mode 100644
index 0000000..1068782
--- /dev/null
+++ b/modules/smartqueue_entities/smartqueue_entities.info
@@ -0,0 +1,9 @@
+name = Smartqueue Entities
+description = Automatically creates a subqueue per entity. For example to have a separate subqueue for each taxonomy term. 
+package = Entityqueue
+core = 7.x
+
+dependencies[] = entityqueue
+
+; Views handlers.
+files[] = smartqueue_entities_handler_relationship_subqueue.inc
\ No newline at end of file
diff --git a/modules/smartqueue_entities/smartqueue_entities.install b/modules/smartqueue_entities/smartqueue_entities.install
new file mode 100644
index 0000000..dbb4700
--- /dev/null
+++ b/modules/smartqueue_entities/smartqueue_entities.install
@@ -0,0 +1,13 @@
+<?php
+/**
+ * @file
+ * Smartqueue Entities installation and update functions.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function smartqueue_entities_uninstall() {
+  // @todo ensure our subqueues are removed before being disabled
+  variable_del('smartqueue_entities_by_source');
+}
\ No newline at end of file
diff --git a/modules/smartqueue_entities/smartqueue_entities.module b/modules/smartqueue_entities/smartqueue_entities.module
new file mode 100644
index 0000000..b1d8080
--- /dev/null
+++ b/modules/smartqueue_entities/smartqueue_entities.module
@@ -0,0 +1,148 @@
+<?php
+/**
+ * @file
+ * Automatically creates a subqueue per entity. For example to have a separate subqueue for each taxonomy term.
+ */
+
+/**
+ * Implements hook_ctools_plugin_directory().
+ */
+function smartqueue_entities_ctools_plugin_directory($module, $plugin) {
+  return 'plugins/' . $module . '/' . $plugin;
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function smartqueue_entities_entity_insert($entity, $entity_type) {
+  $handlers = smartqueue_entities_get_handlers_for_entity($entity_type, $entity);
+  foreach ($handlers as $handler) {
+    if ($handler instanceof SmartQueueEntitiesQueueHandler) {
+      $subqueue = $handler->createSubqueueForEntity($entity);
+      entity_save('entityqueue_subqueue', $subqueue);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function smartqueue_entities_entity_update($entity, $entity_type) {
+  $handlers = smartqueue_entities_get_handlers_for_entity($entity_type, $entity);
+  foreach ($handlers as $handler) {
+    if ($handler instanceof SmartQueueEntitiesQueueHandler) {
+      $subqueue = $handler->loadSubqueueForEntity($entity);
+      if (!$subqueue) {
+        $subqueue = $handler->createSubqueueForEntity($entity);
+      }
+      $subqueue->label = $handler->getSubqueueLabel($subqueue);
+      entity_save('entityqueue_subqueue', $subqueue);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function smartqueue_entities_entity_delete($entity, $entity_type) {
+  $handlers = smartqueue_entities_get_handlers_for_entity($entity_type, $entity);
+  foreach ($handlers as $handler) {
+    if ($handler instanceof SmartQueueEntitiesQueueHandler) {
+      $subqueue = $handler->loadSubqueueForEntity($entity);
+      if ($subqueue) {
+        entity_delete('entityqueue_subqueue', $subqueue->subqueue_id);
+      }
+    }
+  }
+}
+
+/**
+ * Selects just the target bundles dropdown to be returned for re-rendering.
+ *
+ * @return array
+ *  A renderable array representing the target bundles dropdown.
+ */
+function smartqueue_entities_entity_type_callback($form, $form_state) {
+  return $form['handler_settings']['smartqueue_entities']['source_bundles'];
+}
+
+/**
+ * Form #element_validate callback: Cleanup source bundles in the form state.
+ */
+function smartqueue_entities_element_validate_source_bundles($element, &$form_state) {
+  $value = array_filter($element['#value']);
+  form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Get a list of smartqueue handlers interested in the given entity.
+ */
+function smartqueue_entities_get_handlers_for_entity($entity_type, $entity) {
+  $handlers = array();
+
+  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+  if (!$bundle) {
+    $bundle = $entity_type;
+  }
+  $queues = variable_get('smartqueue_entities_by_source', array());
+  $queues_to_load = array();
+  if (isset($queues[$entity_type])) {
+    foreach ($queues[$entity_type] as $queue_name => $source_bundles) {
+      if (in_array($bundle, $source_bundles)) {
+        $queues_to_load[]= $queue_name;
+      }
+    }
+  }
+
+  if (empty($queues_to_load)) {
+    return $handlers;
+  }
+
+  $entityqueues = entityqueue_queue_load_multiple($queues_to_load);
+  foreach ($entityqueues as $entityqueue) {
+    $handlers[] = entityqueue_get_handler($entityqueue);
+  }
+  return $handlers;
+}
+
+/**
+ * Implements hook_views_data_alter().
+ */
+function smartqueue_entities_views_data_alter(&$data) {
+  $entity_types = variable_get('smartqueue_entities_by_source', array());
+  foreach ($entity_types as $entity_type => $smartqueues) {
+    $source_info = entity_get_info($entity_type);
+    $source_base_table = $source_info['base table'];
+    $source_id_field = $source_info['entity keys']['id'];
+
+    foreach ($smartqueues as $smartqueue => $bundles) {
+      $relationship =& $data[$source_base_table]['smartqueue_entities_' . $smartqueue . '_relationship'];
+      $relationship['relationship'] = array(
+        'handler' => 'smartqueue_entities_handler_relationship_subqueue',
+        'base' => 'entityqueue_subqueue',
+        'base field' => 'source_entity_id',
+        'real field' => $source_id_field,
+        'label' => t('@source Smartqueue @queue', array(
+          '@source' => $source_info['label'],
+          '@queue' => $smartqueue,
+        )),
+        'group' => t('Entityqueue'),
+        'title' => t('@source Smartqueue @queue', array(
+          '@source' => $source_info['label'],
+          '@queue' => $smartqueue,
+        )),
+        'help' => t('Create a relationship from @source to its @queue smartqueue.', array(
+          '@source' => $source_info['label'],
+          '@queue' => $smartqueue,
+        )),
+        'extra' => array(
+          array(
+            'field' => 'queue',
+            'value' => $smartqueue,
+          ),
+        ),
+        'smartqueue_entities_queue' => $smartqueue,
+      );
+    }
+  }
+}
diff --git a/modules/smartqueue_entities/smartqueue_entities_handler_relationship_subqueue.inc b/modules/smartqueue_entities/smartqueue_entities_handler_relationship_subqueue.inc
new file mode 100644
index 0000000..3fd1f6a
--- /dev/null
+++ b/modules/smartqueue_entities/smartqueue_entities_handler_relationship_subqueue.inc
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * @file
+ * Specialized relationship handler to add smartqueues.
+ */
+
+class smartqueue_entities_handler_relationship_subqueue extends views_handler_relationship {
+
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['target_relationship']['default'] = FALSE;
+
+    return $options;
+  }
+
+  /**
+   * Default options form that provides the label widget that all fields
+   * should have.
+   */
+  function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    // Load the Entityqueue
+    $queue = entityqueue_queue_load($this->definition['smartqueue_entities_queue']);
+    if ($queue) {
+      $info = entity_get_info($queue->target_type);
+      $target_table = $info['base table'];
+
+      $relationship_options = array();
+
+      if ($this->view->base_table == $target_table) {
+        $relationship_options[$this->view->base_table] = t('Base @type', array('@type' => $info['label']));
+      }
+      foreach ($this->view->display_handler->get_option('relationships') as $relationship) {
+        if ($relationship['table'] == $target_table) {
+          $handler = views_get_handler($relationship['table'], $relationship['field'], 'relationship');
+          $relationship_options[$relationship['id']] = $handler->definition['label'];
+        }
+      }
+    }
+
+    if (!empty($relationship_options)) {
+      $form['target_relationship'] = array(
+        '#type' => 'select',
+        '#title' => t('Target entity relationship'),
+        '#options' => array(
+          '' => t('none'),
+        ) + $relationship_options,
+        '#default_value' => $this->options['target_relationship'],
+      );
+    }
+  }
+
+  function query() {
+    // Figure out what base table this relationship brings to the party.
+    $table_data = views_fetch_data($this->definition['base']);
+    $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field'];
+
+    $this->ensure_my_table();
+
+    $def = $this->definition;
+    $def['table'] = $this->definition['base'];
+    $def['field'] = $base_field;
+    $def['left_table'] = $this->table_alias;
+    $def['left_field'] = $this->real_field;
+    if (!empty($this->options['required'])) {
+      $def['type'] = 'INNER';
+    }
+
+    if (!empty($this->definition['extra'])) {
+      $def['extra'] = $this->definition['extra'];
+    }
+
+    if (!empty($def['join_handler']) && class_exists($def['join_handler'])) {
+      $join = new $def['join_handler'];
+    }
+    else {
+      $join = new views_join();
+    }
+
+    $query = db_select('entityqueue_subqueue', 'sq')
+      ->fields('sq')
+      ->condition('sq.queue', $this->definition['smartqueue_entities_queue']);
+    $query->addExpression('SUBSTRING(sq.name, LENGTH(sq.queue)+2)', 'source_entity_id');
+    $def['table formula'] = $query;
+
+    $join->definition = $def;
+    $join->options = $this->options;
+    $join->construct();
+    $join->adjusted = TRUE;
+
+    // use a short alias for this:
+    $alias = $def['table'] . '_' . $this->table;
+
+    $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship);
+
+    // Load the Entityqueue
+    $queue = entityqueue_queue_load($def['smartqueue_entities_queue']);
+    if ($queue) {
+      // Get the target
+      $field_name = _entityqueue_get_target_field_name($queue->target_type);
+      $target_info = entity_get_info($queue->target_type);
+      if ($field = field_info_field($field_name)) {
+        $field_table = _field_sql_storage_tablename($field);
+
+        $field_def = array(
+          'table' => $field_table,
+          'field' => 'entity_id',
+          'left_table' => $this->alias,
+          'left_field' => 'subqueue_id',
+          'extra' => array(
+            array(
+              'field' => 'entity_type',
+              'value' => 'entityqueue_subqueue',
+            ),
+          ),
+        );
+        if (!empty($this->options['target_relationship'])) {
+          $field_def['extra'][] = array(
+            'field' => $field_name . '_target_id',
+            'left_table' => $this->options['target_relationship'],
+            'left_field' => $target_info['entity keys']['id'],
+            'operator' => '=',
+          );
+        }
+
+        if (!empty($this->options['required'])) {
+          $field_def['type'] = 'INNER';
+        }
+
+        if (!empty($def['join_handler']) && class_exists($def['join_handler'])) {
+          $field_join = new $def['join_handler'];
+        } else {
+          $field_join = new views_join();
+        }
+        $field_join->definition = $field_def;
+        $field_join->construct();
+        $field_join->adjusted = TRUE;
+
+        $this->query->add_table($field_table, $this->alias, $field_join);
+      }
+    }
+
+
+    // Add access tags if the base table provide it.
+    if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) {
+      $access_tag = $table_data['table']['base']['access query tag'];
+      $this->query->add_tag($access_tag);
+    }
+  }
+
+}
\ No newline at end of file
-- 
2.11.0 (Apple Git-81)

