Friday, May 31, 2013

How to add a driving directions link to a marker popup in a Drupal Map

I was tasked at work today with finding a good, lightweight way to display multiple locations on a map, and somehow provide directions to each location. If it looked really cool, well, bonus! The solution that I came up with was just one of many many possible solutions.  For an excellent overview of Drupal Mapping in general, see https://drupal.org/node/1704948. That article was my starting point.  I had several important constraints, however.  Although it would have certainly been possible to replace the geolocation storage that we were already using, I wanted to first see if I could make it work with what we had.  Our initial setup was the following:

Content Type with the following fields:

Address - Provided by the addressfield module.
Map - Provided by the geofield module, requires latitude and longitude.*

* I am tempted to add the geocode module, as it will populate a geofield field based on an addressfield field, but for simplicity's sake, I left it out for now.

My second constraint was that I wanted it to be as simple as possible for the end user, i.e., they add a location with an address and lat/long and everything else is generated automatically. The solution that I came up with uses the leaflet module (see list for link)

Modules:

Addressfield - https://drupal.org/project/addressfield
Geofield - https://drupal.org/project/geofield (version 7x-1.1)
Leaflet - https://drupal.org/project/leaflet
Leaflet More Maps - https://drupal.org/project/leaflet_more_maps
Get Directions - https://drupal.org/project/getdirections
Views - https://drupal.org/project/views*

* Be sure you enable the views_ui module, or you won't be able to create your map view!

Setup is quite simple.  Download and enable all of the modules.  The only one that requires separate configuration is the getdirections module (admin/config/services/getdirections), and the defaults are fine to start with. That said, it is probably best to go into those settings and select 'Use Googlemaps version 3', as that will remove the need for a google api key. Create a content type, call it whatever you want, we'll go with Zingzang, for the purposes of this tutorial, and add an address field(address) and a geolocation field(map).  Now add several test Zingzang nodes, and be sure you fill in the lat/long, that is important.

Now for the view: create a new view, with content of type: Zingzang, and create a page.  Give it a path of /map, or location, or whatever you like. Set the pager to: Display all items. Format the view as Leaflet Map, with these settings:
Data Source = Content:Map
Title Field = Content:Title
Description Content (This one is important!) = Content:Nid

If you installed Leaflet more maps, you will have about 20 styles to choose from in the Map dropdown, I personally like Stamen Toner, fwiw.  Pick a style, and then click on Apply.

Now for the fields. Add these fields:
Content: Map
   Settings: Don't touch the settings, the view formatter takes care of this.
Content: Title
   Settings: Exclude from display, *and* Rewrite the output of this field as: Get Directions to [title]
Content: Nid
   Settings: Rewrite the output of this field as: [title]  *and* Output this field as a link, Link Path is: getdirections/location/to/[nid]

Save your view.  Now you can navigate to your sweet map!  It should look something like this:


The Get Directions to Another Location link will open another page, that should look something like this:



And that's it!  A (relatively) lightweight, super cool looking way to display locations on a map with a directions popup.

As always, please feel free to leave a comment if you have any questions.  I can't promise that I'll be able to answer them, but it's worth a shot... ;)

Wednesday, May 22, 2013

How to rewrite a views field based on the value of a separate field.

Recently at work I was configuring a view of staff members.  Staff could be a Team Leader,  a Team Leader *and* a Board Member, or neither.  An integer list field, field_staff_leader, gave these options the value of 1, 2, and 0, respectively.  The view itself displayed team leaders, so obviously I added a simple filter that only displayed staff members that had a value of 1 or 2 in field_staff_leader.  Simple enough.  The tricky part was the value of another (text) field, field_role.  This field needed to be displayed normally if the value of field_staff_leader was 1 (is a Team Leader), but if the value was 2 (Team Leader and Board Member), field_role needed to be overwritten with the text "Board Member".

There's a hook for that! hook_views_pre_render does exactly what I needed.  Here's the code
/**

 * Check for Board Member status and overwrite field_role if TRUE .

 */

function mymodule_views_pre_render(&$view) {

  $results = &$view->result;

    foreach ($results as $key => $result) {

      if (($view->name == 'staff') && ($view->current_display == 'panel_pane_1')) {

        $field_board = $result->_field_data['nid']['entity']->field_staff_leader;

        if ($field_board['und'][0]['value'] == '2') {

          $results[$key]->field_field_role[0]['rendered']['#markup'] = 'Board Member';

      }

    }

  }
 
To break it down: First you need to access the view's results, and then of course make sure that you're applying your code to the correct view display.  If you are not sure what the view name or display id is, add dsm($view); to your code (be sure you have the devel module enabled), and your needed values will be in $view->name, and $view->current_display, respectively. 
/**

 * Check for Board Member status and overwrite field_role if TRUE .

 */

function mymodule_views_pre_render(&$view) {

  $results = &$view->result;

    foreach ($results as $key => $result) {

      if (($view->name == 'staff') && ($view->current_display == 'panel_pane_1')) {
Once you have targeted the correct view/display, check the field value:

        $field_board = $result->_field_data['nid']['entity']->field_staff_leader;

        if ($field_board['und'][0]['value'] == '2') {
...and then add your custom markup:
          $results[$key]->field_field_role[0]['rendered']['#markup'] = 'Board Member';
Then you may go enjoy your fully functional Death Star. Er, View.

Thursday, May 2, 2013

How to write a custom ctools access plugin for a Boolean field.

I had the delightful task at work of determining the visibility of a view (in a node variant) based on the value of a boolean field.  But it got better!  Said boolean field was not on the node that was being viewed, it was on a node that referenced the node being viewed.  But wait!  It gets better!  The referenced node was an organic group, and those references work differently then a usual entity reference!  Confused yet?  Yeah, me too.  I got it in the end though, and here it is, on the vague assumption that such an odd use case will ever come up again...

First things first: you have to let ctools know that there is a plugin to use.  In your custom module (or in my case, in the relevant feature) .module, you need something like this:
/**
 * Implements hook_ctools_plugin_directory().
 *
 * It simply tells panels where to look for the .inc file that
 * defines various args, contexts and content_types.
 */
function my_feature_ctools_plugin_directory($module, $plugin) {
 if ($module == 'ctools' && !empty($plugin)) {
   return "plugins/$plugin";
 }
}



Then in your feature (or custom module), create the directory, so you have a my_feature/plugins/access folder structure.

In the access folder, create a new .inc file.  For the purposes of this exercise, the field we are using to determine visibility will be called field_widget, so I would call my file field_widget.inc.

You have to start by defining the plugin, so:
<?php

/**
 * Plugins are described by creating a $plugin array which will
 * be used by the system that includes the file.
 */
$plugin = array(
  'title' => t('Node: Widget'),
  'description' => t('Only displays this pane if the Widget field on the related Home Page for this Organic Group is set to On.'),
  'callback' => 'my_feature_field_widget_ctools_access_check',
  'default' => array('field_widget' => 1),
  'summary' => 'my_feature_field_widget_ctools_access_summary',
  'required context' => new ctools_context_required(t('Node'), 'node'),
);  



Now write the callback:
/**
 * Custom callback defined by 'callback' in the $plugin array.
 *
 * Check for access.
 */
function my_feature_field_widget_ctools_access_check($conf, $context) {

  // If for some unknown reason that $context isn't set, return false.
  if (empty($context) || empty($context->data)) {
    return FALSE;
  }

Because the field is *not* on the node that is currently being viewed, we have to identify the correct home page node.
  // Identify the home page node for the current organic group.
  $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'node')
      ->entityCondition('bundle', 'division_home')
      ->fieldCondition('og_division_home_division_ref', 'target_id', $context->data->nid);
    $result = $query->execute();




Now we have to load the node, to access the field_widget value.
    $home_page_nid = current($result['node'])->nid;
    $home_page_node = node_load($home_page_nid);


Here is where we check the value of field_widget.
  Being a boolean, if the value is 0, that means the boolean is not checked, and we want to hide the pane.
// If the home page widget field is not checked, hide the pane.
  if (!$home_page_node->field_widget['und'][0]['value']) {
  }
Otherwise, show the pane.
  return TRUE;
}



Here is the whole kit and caboodle:
<?php

/**
 * Plugins are described by creating a $plugin array which will
 * be used by the system that includes the file.
 */
$plugin = array(
  'title' => t('Node: Widget'),
 
 'description' => t('Only displays this pane if the Widget field on 
the related Home Page for this Organic Group is set to On.'),
  'callback' => 'my_feature_field_widget_ctools_access_check',
  'default' => array('field_widget' => 1),
  'summary' => 'my_feature_field_widget_ctools_access_summary',
  'required context' => new ctools_context_required(t('Node'), 'node'),
);   

/**
 * Custom callback defined by 'callback' in the $plugin array.
 *
 * Check for access.
 */
function my_feature_field_widget_ctools_access_check($conf, $context) {

  // If for some unknown reason that $context isn't set, return false.
  if (empty($context) || empty($context->data)) {
    return FALSE;
  }



  // Identify the home page node for the current organic group.
  $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'node')
      ->entityCondition('bundle', 'division_home')
      ->fieldCondition('og_division_home_division_ref', 'target_id', $context->data->nid);
    $result = $query->execute();

    $home_page_nid = current($result['node'])->nid;
    $home_page_node = node_load($home_page_nid);



// If the home page widget field is not checked, hide the pane.
  if (!$home_page_node->field_widget['und'][0]['value']) {
  }

  return TRUE;
}


And there you have it.  :)

Friday, March 8, 2013

How to delete a Drupal field with field_delete_instance

So here's the scenario: you've working with a bunch of other people on a Drupal site, and content types are being managed with Features.  Someone (probably you, but let's be generous and say it was someone else) has added a field to a content type that needs to be removed.  No problem!  Go into the feature, create a feature_name.install file, and add this code:

<?php

/**
 * @file
 *   Install and update scripts for the feature_name feature.
 */

/**
 * Delete my_field
 */
function feature_name_update_7001(&$sandbox) {
  // Remove the my_field field.
  field_delete_field("my_field");
  field_purge_batch(1);
}

Recreate your feature without the field you are removing, run update.php, and there you go.

"But wait!", you say. " Stop!", you say.   That wasn't the only place I was using my_field, and I want to keep it in the other content types!!  If I run that script, it will remove all instances of the field on my site! 

You are so right.  It will.  I've been bitten by that one myself....  If you want to remove only one instance of a field, you need field_delete_instance instead, and you want it in an update script so that it only fires once.

<?php

/**
 * @file
 *      Install and update scripts for the feature_name feature.
 */

/**
 * Implements hook_update_N() to remove the my_field field from *only* the feature_name ct.
 */
function feature_name_update_7001(&$sandbox) {
  // Remove my_field from *only* the specified content type.
  $instance = field_info_instance('node', 'my_field', 'content_type_name');
  field_delete_instance($instance, TRUE);
  field_purge_batch(1);
}

The field_info_instance is this:
$instance = field_info_instance('entity_type', 'field_name', 'bundle');

Helpful hint: you can find this information in the field_config_instance table of your sites database. 

And there you have it.  The field will be removed from the specified content type, left alone everywhere else, and no one has to go in and remove it in the UI.

Wednesday, January 2, 2013

How to validate the dimensions of an image, and warn the user if it is too small in Drupal

  Isn't that a magical title?  Titles are surprisingly hard to write for this sort of thing.  They are either short and entirely unhelpful, or way too long, but descriptive. At any rate, this is episode 1 of "Learning from the mistakes of others", others, in this case, being me.  Of course.
  So I was assigned an issue at work (as if I don't have plenty of my own issues)(hur hur).  The site we are working on has slideshows on several pages, that are populated by images automatically imported from feed items.  This means there is a very good chance that some of the images that come in with the feed item will be too small, and would look bad in the slideshow.  We needed to verify that any given image was big enough for the slideshow, and throw up a warning notice if it wasn't. Easy peasy, right?  Erm, no.  Not for me.  As it turns out, having no background whatsoever in php/(any programming language at all) makes for a long, hard slog through the Drupal API documents....  Fortunately, I have some awesome co-workers that don't mind helping a girl out when I get stuck.  Which is... frequently.
  If I were to go through all of the iterations of (let's be honest here) psuedo-code that I tried, we would be here all night.  Eventually, one of my worker buddies threw a bucket of cold water over me(figuratively speaking, of course), and pointed out something very valuable.  When coding, you just have to take it step by step.  Write down what you need to do, the order in which it needs to be done, and *then* begin coding.  After each step, check that it is working as you expect it to.  dpm() is your friend.  Also, turn on error reporting in your settings.php if you are working in a local environment.  That saved me oooooodles of time once I did it.  Here is my final product, although keep in mind that it hasn't been given the stamp of approval yet by my superiors, so it is *entirely* possible that there are better ways to do this.  It works, is all I know... ;) I've gone through and commented it more thoroughly, just to be super specific about what I did.

/**
 * Implements hook_form_form_id_alter().
 */
function my_module_form_my_form_id_form_alter(&$form, &$form_state, $form_id) {
  //This is where I've added my custom validate function.
  $form['#validate'][] = 'my_module_image_form_validate';
}





/**
 * Implements hook_node_validate() and image_style_load().
 *
 * Check image dimensions and return warning if image is too small.
 */
function my_module_image_form_validate(&$form, &$form_state) {
 
  // Check if fid exists. This prevents any possible errors if there isn't an image in the field.
  if (isset($form_state['values']['field_media']['und']['0']['fid'])) {
  // Get the fid.
  $fid = ($form_state['values']['field_media']['und']['0']['fid']);
  // You have to load the file before you can check its dimensions, so:
 $file = file_load($fid);

  // Now that the file is loaded, I can set my variables. Use dpm($file) to find them...
  $height = ($file->image_dimensions['height']);
  $width = ($file->image_dimensions['width']);

  // I originally had this without the next 7 lines, and the numbers were hardcoded, i.e., if ($width < 780) etc...  A wise co-worker pointed out that this would cause trouble if the image styles were ever changed, and that I should use the image style presets.  So next I:
  // Get the image style presets.
  $styles = image_styles();
  // Now I can set my variables to check against.
  $minheight = ($styles['iin_wide_780x438']['effects']['15']['data']['height']);
  $minwidth = ($styles['iin_wide_780x438']['effects']['15']['data']['width']);
  //dsm($styles); (always check your work)


  // And here is the magic:
  if (isset($height) && isset($width)) {
  if ($width < $minwidth || $height < $minheight) {

  // This just sets a message right above the image field (field_media) to warn the user that the image is too small.
  $form['field_media']['#prefix'] = '<div class="messages warning">Image is smaller than recommended size.</div>';
      }
    }
  }
}
And KA-BLOW!  Bob, so to speak, is your uncle!
And now I must go to bed.
Minion out.

Friday, December 21, 2012

How to display one of two possible Drupal fields in a node template (hint: it's easier then you think!)

Yesterday at work, I was asked to set up a field in a node template panel that would display a field called 'story' if it had content, but default back to the nodes' 'body' field if the 'story' field was empty.  This is possible using views, but it was going to require an argument to be passed from the panel to the view to allow the view to only display the content from the node being viewed. (Or edited)  In theory, I knew what to do, but I did a little research anyway, as I like to do, and I ran across this excellent post: http://blog.urbaninsight.com/2012/05/14/how-to-conditionally-display-value-from-two-fields-in-views

That confirmed my plan for the fields, but I also needed to pass an argument from the panel to the view.  This, in the end, is how I set it up:

(I am assuming here that your content type already contains *both* fields that you are trying to conflate.  I am also assuming that they are both text fields, if one of them is something fancy, like a user reference field, you will need to use a relationship to be able to add the field into your view.)

Create a view, name it, select the content type that you will be displaying fields from.  Do not create a page or a block.  Click Continue & Edit.  Click the +Add button at the top of the page, and add a content pane.  This is important.  It needs to be a content pane.  Content pane.  Got it?  Good.  Now add your body field.  In its settings, select, Exclude from display.  Now add your story field.  The order is important.  I promise.  In the story field settings, expand the 'No results behavior' section.  In the 'No results text box', you are going to put the replacement pattern for your field.  If you don't happen to know it, in the 'Rewrite the output of this field' section, there is an expandable list of replacement patterns, in which you will be able to find your previous field.  This is why the order is important.  You cannot rewrite a field in a view with content from a later field.  At any rate, the text that I needed in my 'No results text' box were this:
[body]

That's it. The Body field will display if there is text in it, and the Story field will display instead if the Body field is empty.  Bada-bing, bada-boom. (Don't forget to save your view)

Now, how, you ask, does one set the view to only display the field from the node being viewed.  Well, in my case, I passed an argument from the panel to the view content pane, like so:

Add a contextual filter: Content:Nid
Under Pane settings, click on edit for the Argument input.  Select 'Input on pane config'.
Now go to your panel, in my case, the node variant for that content type, and add your view.  (It will be in the View panes catagory.) The only setting that will be available for the field will be the argument text field.  In it (at the advice from a much wiser colleague) I put '&node:nid'.  This is very specific.  Save the panel, and you should be good to go.


Thursday, December 13, 2012

Drupal 101: Lesson 1

Ok boys and girls, here begins Drupal 101!

:)

First assignment:

  • Get a notebook to use for this "class".  You will be writing down passwords and user id's that you will need to have later, and it will be very annoying to have forgotten. ;)
  • Now go to:  https://docs.acquia.com/user/register
  • Create an account.  You are not buying anything, I promise. ;) Write down the user id and password
  • Now go to: https://www.acquia.com/downloads  and download the Dev Desktop Drupal 7 package.  This is free, Drupal is open source, and you should not ever have to pay for any of its components. 
  • There are installation instructions at this link: https://docs.acquia.com/dev-desktop/install
  • Write down anything that you have to fill out during the installation process.  User ID, email, password, Database name, etc...
In my opinion, and after trying out several different setups, I've decided that Acquia is by far the easiest way to get started with a local development environment. I don't work for them, and I am not affiliated with them in any way, for what it is worth. 

* I will not ever tell you to download anything that has to be paid for!!!  Everything that you will be using is open source, i.e. FREE!

EXTRA CREDIT

  • Open Terminal (Applications->Utitlities->Terminal)
  •  Type drush status 
    • What do you see? (answer is at the bottom of the page)

EXTRA EXTRA CREDIT

  • Read this: http://soundpostmedia.com/article/5-reasons-youll-love-using-drush-drupal
  • Watch this: http://www.leveltendesign.com/tutorial/video/drupal-7-overview

ANSWERS:

You should see something that looks like this:

 NAME OF YOUR COMPUTER:~ nameofyouruser$ drush status
 PHP configuration     :  /Applications/acquia-drupal/php5_3/bin/php.ini
 Drush version         :  5.7                                           
 Drush configuration   :