Thursday, 11 July 2013

Export UI, Features and Taxonomy

Here's a quicky. Let's say you've created some exportable content (using CTools) which references a term ID and you have to go from your dev site to the production site. And your taxonomy is also being exported using UUID.

Somehow you have to tie them together because sure as eggs is eggs the term IDs created on the production site are not going to be the same as the local ones. That's why you used UUID in the first place. Right?

Here's what you do: in your local exportable you will have a column for the term ID so include a column for the term UUID as well.

In your add/edit form for the exportable you'll have to include some code to automatically add the selected term's UUID - I did it in the form validation. Basically you read the selected term ID, load the selected term which will have the UUID in it (because it's added to the base table). Set the term UUID value in $form_state['values']. Assuming you're using CTools Export UI the UUID will be saved automatically.

Also, in the module install, add "no export" => TRUE to the TID field, so that Export UI does not include it in the feature. (This code works for Features, it doesn't work for single imports, I'll leave that as an exercise for the reader - hint: you can specify an "import callback".)

That's the easy bit, when you export your content it will be saved with the term's UUID and not the term's ID. The difficult bit is how to link the UUID of exported content when Features loads it into the new site.

Except it's not hard at all. In your export specification, in the schema, you have the "default hook", well CTools Export UI very kindly calls an drupal_alter() on the default items after it's loaded them. So we can do this:

/**
 * Implements hook_DEFAULT_HOOK_alter().
 *
 * This intercepts any defaults picked up from code and converts
 * their UUID category into the local TID (which might be different
 * on every site).
 *
 */
function mymodule_my_default_hook_alter(&$items) {
  $uuids = db_select('taxonomy_term_data', 't')
      ->fields('t', array('uuid', 'tid'))
      ->execute()->fetchAllKeyed();

  foreach ($items as $item) {
    if (empty($item->tid) && !empty($uuids[$item->uuid])) {
      $item->tid= $uuids[$item->uuid];
    }
  }
}

The database call creates an array which maps all UUIDs to TIDs in one go. If your site uses a lot of taxonomy terms - perhaps you have user tagging - you might want to restrict this call to a specific vocabulary.

The exact item property names will depend on what you set up in your schema.

Sorted.

Friday, 5 July 2013

Entity Reference Views Widget plus Organic Groups Nightmare

Let's suppose you are using the Entity Reference Views Widget to display items to be included in an Entity Reference field. This is actually quite a cool module while being a little awkward to use - essentially it gives you a views listing of candidate entities to add, with an AJAX-driven checkbox: click the box and the entity gets moved to the list on the left to display what's been chosen.

Which is all fine.

The project I'm currently working on uses Organic Groups to group certain users within an organisation allowing them to work on very specific types of node content. I had to create a completely new type of entity (though that's not important) for them so they could add one or more of these entities to one or more of their special content.

The new entity was made subject to the OG, and that too was fine.

So then I came to build the Entity Reference Views Widget to only display the new entities that belonged to the current user's OG.

Meltdown. Either I listed everything, or nothing. Filtering OGs is not the easiest thing in the world: You have to create an OG relationship for the entity in question (easy) and then add an argument which if it has no value (the desired state) uses the OG Context module to figure out what OGs are available.

Weird fact number #87654: The OG context module allows you to base OG on the current node and on a user currently being viewed or edited, but not on the current user. So I had to build a quick context for that:

/**
 * Implements hook_og_context_negotiation_info().
 */
function mymodule_og_context_negotiation_info() {
  return array(
    'user' => array(
      'name' => t('User'),
      'description' => t("Determine context by finding the current user's OG (if any)."),
      'callback' => 'mymodule_context_handler_user',
    ),
  );
}

/**
 * Implements hook_og_context_negotiation_info_alter().
 */
function mymodule_og_context_negotiation_info_alter(&$contexts) {
  $context['node']['menu path'][] = 'node/%/edit';
}

function mymodule_context_handler_user() {
  global $user;
  $account = clone $user;
  $contexts = _group_context_handler_entity('user', $account);
  return $contexts;
}

In fact this does two things: it expands the context checking for nodes to include nodes being edited and adds a context that looks at the current user.

Okay. Next factor: Entity Reference Views Widget has this neat facility for feeding the entity IDs of entities already selected back into the view and excluding them. This is great and it also uses an argument, which needs to be the first argument.

However, and this is the nastiness, if the ERVW argument does not exist (i.e. no entities have yet to be selected for the field) the second argument fails to fire and you see all the entities without any OG filtering.

The solution is thankfully quite simple: Edit the ERVW argument so that it has a default value of "all", this means it always exists and the second argument does fire and figure out the correct OG context puts it into the query and filtering actually works.

Obviously I went through various stages of thinking each handler was broken or that there was something weird about my newly created entity. But none of those things were true. The problem was simply one of configuration.

Hope that helps.