diff --git a/entity.api.php b/entity.api.php index 130e1ce..4b6e1d3 100644 --- a/entity.api.php +++ b/entity.api.php @@ -33,6 +33,9 @@ * specify the name of the fieldable entity type. But note, that the usual * information about the bundles is still required for the fieldable entity * type, as described by the documentation of hook_entity_info(). + * - single-bundled: Whether the entity has more than one bundle, or is capable + * of having more than one. Eg, 'user' is single-bundled; 'node' is not, even + * if there is currently only one node type defined. * - module: The module providing the entity type. Optionally, but suggested. * - exportable: (optional) Whether the entity is exportable. Defaults to FALSE. * If enabled, a name key should be specified and db columns for the module diff --git a/entity.info b/entity.info index 958e28c..0753b1e 100644 --- a/entity.info +++ b/entity.info @@ -10,3 +10,4 @@ files[] = entity.features.inc files[] = entity.info.inc files[] = entity.rules.inc files[] = entity.test +files[] = views/handlers/entity_handler_relationship_limit_type.inc diff --git a/entity.module b/entity.module index 45d2231..488f790 100644 --- a/entity.module +++ b/entity.module @@ -1153,6 +1153,23 @@ function entity_entity_info_alter(&$entity_info) { if (!isset($info['configuration'])) { $entity_info[$type]['configuration'] = !empty($info['exportable']); } + + // Add a flag to indicate whether the entity is permanently single-bundled. + // For example, 'user' is single-bundled, but 'node' is not even if there + // happens to be only one node type at the time. + // Note: the detection for this is possibly a little flaky, and relies on + // the fact that entities with bundles specify an admin path that includes + // an an autoloader wildcard for the bundle name. + $entity_info[$type]['single-bundled'] = FALSE; + if (count($info['bundles']) == 1) { + // Get the details of the only bundle. + $bundle = array_shift($info['bundles']); + // Single-bundled entities either have no 'admin' setting because there is + // no admin for them, or their admin path does not contain a wildcard. + if (!isset($bundle['admin']) || strpos($bundle['admin']['path'], '%') === FALSE) { + $entity_info[$type]['single-bundled'] = TRUE; + } + } } } diff --git a/views/entity.views.inc b/views/entity.views.inc index aef255a..4c6a2fa 100644 --- a/views/entity.views.inc +++ b/views/entity.views.inc @@ -147,25 +147,30 @@ class EntityDefaultViewsController { // Prepare reversed relationship data. $label_lowercase = drupal_strtolower($this->info['label'][0]) . drupal_substr($this->info['label'], 1); $property_label_lowercase = drupal_strtolower($property_info['label'][0]) . drupal_substr($property_info['label'], 1); - + // Determine the handler to use for the relationship. + $handler = $this->getRelationshipHandlerClass($type, $this->type); $this->relationships[$info['base table']][$this->info['base table']] = array( 'title' => $this->info['label'], 'help' => t("Associated @label via the @label's @property.", array('@label' => $label_lowercase, '@property' => $property_label_lowercase)), 'relationship' => array( 'label' => $this->info['label'], - 'handler' => 'views_handler_relationship', + 'handler' => $handler, 'base' => $this->info['base table'], 'base field' => $views_field_name, 'relationship field' => isset($info['entity keys']['name']) ? $info['entity keys']['name'] : $info['entity keys']['id'], + // Not a Views property, but needed by our relationship handler. + 'entity type' => $this->type, ), ); + $handler = $this->getRelationshipHandlerClass($this->type, $type); $return['relationship'] = array( 'label' => drupal_ucfirst($info['label']), - 'handler' => 'views_handler_relationship', + 'handler' => $handler, 'base' => $info['base table'], 'base field' => isset($info['entity keys']['name']) ? $info['entity keys']['name'] : $info['entity keys']['id'], 'relationship field' => $views_field_name, + 'entity type' => $this->type, ); // Add in direct field/filters/sorts for the id itself too. @@ -302,6 +307,40 @@ class EntityDefaultViewsController { } /** + * Determines the handler to use for a relationship between two entities. + * + * @param $entity_type_left + * The entity type for the entity on the left of the relationship, that is, + * the entity whose views data is being built. + * @param $entity_type_right + * The entity type for the entity on the right of the relationship, that is, + * the entity whose base table the relationship joins to. + */ + function getRelationshipHandlerClass($entity_type_left, $entity_type_right) { + $entity_info_left = entity_get_info($entity_type_left); + $entity_info_right = entity_get_info($entity_type_right); + // If the right side of the relationship join has multiple bundles, then we + // use our relationship handler that allows the relationship to be limited + // by bundle. + if ($entity_info_right['single-bundled']) { + $handler = 'views_handler_relationship'; + } + else { + // However there is a special case: if the relationship is from an entity + // bundle to its bundlee (!), then type limiting is meaningless, hence + // use the basic handler. + if (isset($entity_info_left['bundle of']) && $entity_info_left['bundle of'] == $entity_type_right) { + $handler = 'views_handler_relationship'; + } + else { + $handler = 'entity_handler_relationship_limit_type'; + } + } + return $handler; + } + + + /** * A callback returning property options, suitable to be used as views options callback. */ public static function optionsListCallback($type, $selector, $op = 'view') { diff --git a/views/handlers/entity_handler_relationship_limit_type.inc b/views/handlers/entity_handler_relationship_limit_type.inc new file mode 100644 index 0000000..0e91f3d --- /dev/null +++ b/views/handlers/entity_handler_relationship_limit_type.inc @@ -0,0 +1,100 @@ + array()); + + return $options; + } + + /** + * Add an entity type option. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Get the entity type from where we put it in hook_views_data(). + $entity_type = $this->definition['entity type']; + $entity_info = entity_crud_get_info(); + $entity_info = $entity_info[$entity_type]; + + // Apparently EntityDefaultMetadataController::bundleOptionsList() could do + // this for us, but it's broken: see http://drupal.org/node/1286164. + foreach ($entity_info['bundles'] as $name => $bundle_info) { + $options[$name] = $bundle_info['label']; + } + + $form['entity_type'] = array( + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $this->options['entity_type'], + '#title' => t('@entity types', array('@entity' => $entity_info['label'])), + '#description' => t('Restrict this relationship to one or more @entity types. Selecting more than one type may produce duplicate rows.', array( + '@entity' => strtolower($entity_info['label']), + )), + ); + } + + /** + * Called to implement a relationship in a query. + * + * Mostly the same as the parent method, except we add an extra clause to + * the join. + */ + 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->field; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + // Add an extra clause to the join if there are entity types selected. + $entity_types = array_filter($this->options['entity_type']); + if (count($entity_types)) { + $def['extra'] = array( + array( + // The table and the IN operator are implicit. + 'field' => 'type', + 'value' => $entity_types, + ), + ); + } + + if (!empty($def['join_handler']) && class_exists($def['join_handler'])) { + $join = new $def['join_handler']; + } + else { + $join = new views_join(); + } + + $join->definition = $def; + $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); + } +}