How-Tos

Multilingual CSS generated content in Drupal

CSS generated content is cool. You can make those little triangles everyone seems to love, but its real purpose is to let you add presentational words that would otherwise be a pain to generate in markup for some situations.

a:before {
  content: 'Download item';
}

It can do other neat things like output the contents of another attribute on your element. This feature was originally created to allow print stylesheets to display URLs inline for printed documents:

a:after {
  content: ' (' attr(href) ')';
}

But what happens when you need to bring multilingual into the mix? We occasionally build multilingual sites, and typically there is translatable content at every level of the stack: Drupal nodes, template files, CSS, JS, you name it. When I was originally tasked with solving this it seemed like a big problem, but it turns out to be relatively simple.

My initial thought was to use the language attribute of html tag and make individual, static content properties, but that leads to code bloat as you accommodate each new language. Furthermore, in Drupal’s case, it leaves your strings stranded in CSS and inaccessible to translators which have an interface built into Drupal’s admin UI.

Solution: Dynamically generate attributes

We had a client who wanted the links to be big CTAs accompanied by the instructions “Download item.” So instead of hardcoding the English words into my stylesheet, I added a data-attribute into the DOM via JavaScript (you could also do this in Drupal by creating or modifying the appropriate field.tpl.php, but I did some other things with JS so it was simpler to keep it all in one file).

$('.my-field').attr('data-cta-msg', Drupal.t('Download item'));

Notice Drupal.t in there? t is for translate, and it is the standard mechanism for localizing JS-powered UI components in Drupal 6, Drupal 7, and Drupal 8. There’s a bit of background magic that happens, but in a nutshell once a page containing your new Drupal.t() string has been loaded, you can access it in the Translate interface in the Drupal admin UI:

Drupal 7 interface for translating non-content/UI strings

Now you can grab a translator, add the appropriate text, and your stylesheet will always supply the correct translation in your CSS generated content! Look at the minor modification to CSS content property (compare to the very first example in this article)

a:before {
  content: attr(data-cta-msg);
}

And here’s the result in English and Arabic respectively:

English download button with CSS generated content

Arabic download button with CSS generated content

Update: Down in the comments Dan Mouyard pointed out how any CSS generated content is less accessible than real markup. Just a heads up for general use of CSS content that is intended to be read aloud on the page, not just the multilingual method described in this article.

Jekyll event schedule

Screenshot of DrupalCamp Austin schedule

While working on the DrupalCamp Austin schedule, I was surprised to find that there weren’t any Jekyll templates or plugins available for event schedules. Since it was relatively simple to put one together I want to share some code we generated in the process of hosting the event.

Like many community-driven conferences, DrupalCamp Austin is a multi-day, multi-track event with lunch breaks, keynotes, and evening events like parties. These templates were built to easily handle all of that, so if your event is similar they will probably work for you. Read on for a breakdown of our Jekyll event schedule.

Config

Very little is required in _config.yml file. Simply add pretty permalinks and your individual session pages will be generated at the appropriate locations for internal linking between schedule and detail pages.

permalinks: pretty

Post format

Jekyll posts all start with a date stamp: YYYY-MM-DD, but we treated the filenames slightly different than a normal Jekyll post. By adding some extra numbers related to session times, we can control the order of sessions all happening on one day. Although this is strictly alphabetization and not in any way specifying the time of a Jekyll post, it produces the correct time-based sorting when every session is named with this convention.

_posts/schedule/YYYY-MM-DD-HH-mm-clever-session-title.md

Looking inside a session, we have a pretty basic setup. If you’re familiar with Jekyll at all, you know about YAML Front Matter. Here are the required properties for scheduled sessions:

title: Session title
category: session
published: true
accepted: true
date: 2013-06-22 9:00am

Some are obvious; on a real site we’ll need to categorize or display different types of content and to make this demo have drop-in functionality you need title, category, and published. The other two are specific to our schedule.

  • accepted is a flag that is useful for events with community-submitted content, allowing you to distinguish which sessions you have accepted for inclusion on the schedule. For our event, it’s possible to have a published session submission that is not included on the schedule. Your event may have different rules so feel free to ignore this.
  • date specifies when the session starts, including the time. This property should be considered the canonical source of the session’s time. The filename is merely an ordering mechanism, and the timestamp described previously is not included in the “post date” internal to Jekyll.

Schedule templates

index.html

This is a full Jekyll page, and it prints the entire schedule using this block of code. The only notable thing is the posts are reversed, meaning the schedule will display sessions oldest first, since the default is like a blog with newest posts first.

{% for post in site.categories.session reversed %}
    {% capture day %}{{ post.date | date: "%A" }}{% endcapture %}
    {% if day == 'Saturday' %}
        {% include schedule-day.html %}
    {% endif %}
{% endfor %}

If you have a multi-day event, just duplicate this loop for each day. Our demo code has two days but one is commented out.

schedule-day.html

This template holds most of the important logic for rendering the schedule:

{% if post.room == 'full' and post.accepted == true %}
    {% include session-full.html %}
{% elsif post.accepted == true %}
    {% capture slot %}{% cycle 'begin', 'skip', 'end' %}{% endcapture %}
 
    {% if slot == 'begin' %}
        <section class="slot">
            <h3 class="time">{{ post.date | date: "%I:%M%p" | downcase }}</h3>
    {% endif %}
 
    {% include session.html %}
 
    {% if slot == 'end' %}
        </section>
    {% endif %}
{% endif %}

To create time slots that accomodate any number of tracks, we used a rotating variable via the {% cycle %} tag. Cycle does exactly what you might think and cycles through a list of values supplied by the developer. You can add as many as you want, for example if you want to make our three-track schedule only two tracks, remove the skip value, four tracks would require two instances of skip, and so on.

The markup that opens and closes each time slot is output only when the cycle tag says to. By placing cycle in the room-specific conditional (instead of near the top of the template), ‘full’ slots aren’t counted by cycle and the loop logic stays simple while still accommodating an unpredictable number of full-width slots.

session.html

This template is pretty straightforward, but check out the data attributes. The template takes the YAML values from each post and converts them to a more predictable format for use by CSS/JS. This is accomplished with a few Liquid filters to remove white space and force lowercase. A little further down I’ll discuss some neat uses of data attributes as opposed to classes.

<article class="session"
  data-room="{{ post.room | downcase | replace:' ','-' }}"
  data-track="{{ post.track | downcase | replace:' ','-' }}"
  data-difficulty="{{ post.difficulty | downcase | replace:' ','-' }}">

Singularity and Breakpoint

CSS can obviously be accomplished however you want, but we’re fans of Singularity for grids and Breakpoint for media query management. I’ve provided some barebones CSS to make the schedule function, and beyond that it makes no assumptions. It’s ready for your design.

For the schedule layout we have a responsive grid with one breakpoint: one column until everything can fit, then we switch to a fluid, asymmetric 4-column grid with columns having ratios of 2/3/3/3. Here’s the entire layout in 20 lines:

// breakpoints
$sched-cols: 820px;
 
// grid settings
$grids: 1;
$gutters: 1/4;
$grids: add-grid(2 3 3 3 at $sched-cols);
 
// schedule layout
.schedule {
  section.day, section.slot {clear: both; }
 
  @include breakpoint($sched-cols) {
    h3.time { @include grid-span(1,1); }
    [data-room="room-1"] { @include grid-span(1,2); }
    [data-room="room-2"] { @include grid-span(1,3); }
    [data-room="room-3"] { @include grid-span(1,4); }
    .full { @include grid-span(3,2); }
  }
}

Singularity’s grid-span mixin is pretty slick. First of all it knows about our grid inside the breakpoint mixin due to the definition up top. Second, the syntax is very clean and quick to comprehend. The two arguments are column-span and position. For example: grid-span(1,1) is a single column starting at the first column, grid-span(1,4) is the last column of our grid, and grid-span(3,2) spans 3 columns starting at the 2nd column. See an expanded, commented version of this stylesheet on GitHub.

Data attributes for use by CSS and JS

However you choose to accomplish your layout, I recommend using the data attributes to position each session under the correct heading. Relying on data attributes instead of nth-child() or another markup-specific selector means you don’t have to worry about the exact order that Jekyll outputs the schedule, because the session just falls under the correct heading. This may not sound like a big deal, but renaming files just to accommodate schedule changes is annoying… trust me! Our room headings use data attributes as well, allowing everything to be controlled by a single selector.

It’s also more convenient to use data attributes if you want to write any sort of JS to enhance the schedule. DrupalCamp Austin’s track filters run off of the data-track attribute, allowing you to filter out chunks of the schedule you might not be interested in. Example:

Animated GIF showing DrupalCamp Austin schedule filters in action

Demo and code

That’s what you were looking for anyway, right? I hope you enjoyed the walkthrough, but the real action is in the GitHub repo. Pick our code apart and feel free to suggest improvements!

Although it’s possible to turn this into a plugin, I chose not to. Part of the appeal of small Jekyll sites is free hosting/deployment via GitHub Pages, which currently disallows the use of plugins. Providing templates instead of a plugin means that everyone including GitHub Pages users can make use of the files.

Basic jekyll-schedule demo
Code for jekyll-schedule

If a vanilla example isn’t quite compelling enough, check out the DrupalCamp Austin schedule, with a real design and some nice bells and whistles like sticky headers:

Branded demo
Code for DrupalCamp Austin

One less JPG

I’d like to demo a simple how-to. There are many, many techniques to make pages load faster, but this post attempts to demonstrate large gains from very small code changes.

People often build beautiful sites with multiple easy-to-use JavaScript libraries. Then, when it comes to addressing frontend performance, suddenly those libraries are an enormous download that the users are forced to bear.

Just one image

Before you go worrying about how to minify every last library or shave tests out of Modernizr, try and see if you can remove just one photo from your design. It will make a bigger difference.

Coined by Adam Sontag, the “one less JPG” idea — nay, MOVEMENT — is summed up perfectly here:

Real example

Last year we re-launched Pressflow.org. We have some mobile traffic, but it’s likely people just browsing for info, since no one has a good reason to download Pressflow onto a phone or tablet. Let’s keep their attention and make the experience fast.

We have this huge, beautiful mountain on the homepage. It’s great. But it’s also 160K. I tried making it smaller, or splitting the photo off of the background pattern, but it decreased the quality of the photo too much when I lowered the file size. We made a wonderfully small SVG logo, but that’s not an option for a photograph with this kind of detail.

How much impact does it have?

A mountain is a big thing — just like the amount of traffic Pressflow can handle — and the image we chose was meant to convey that vastness. Since it doesn’t really pack the same punch on smaller screens, why include it at all? I decided to use Modernizr and conditionally load the stylesheet that references the mountain. That way it never gets loaded by tiny screens that don’t need it.

Using the Modernizr Drupal module, I added a conditional load into the .info file of my theme:

; Load CSS with Modernizr
modernizr[Modernizr.mq('screen and (min-width: 42em)')][yep][] = css/big.css

This tells Modernizr to output a Modernizr.load() statement with the test I specified. In this case, Modernizr will only load big.css if the test is true. My test checks the width of the window using a media query — .mq() — and returns true if the screen is at least 42em, causing the CSS to be fetched. Here’s the JavaScript output:

Modernizr.load({
  test: Modernizr.mq('screen and (min-width: 42em)'),
  yep : 'http://pressflow.org/sites/all/themes/pfo/css/big.css',
});

So that’s it, instant savings!

..oh what’s that? Always test your work? Thanks for keeping me honest.

Here’s some data.

I’ve got two network waterfalls here for comparison. They show a pretty stark difference following this one-line change to my code. If a screen isn’t big enough for the mountain, it’ll only take 20 HTTP requests and 193K total. If the screen is big enough, it takes 24 HTTP requests — for the CSS and then the images inside it — totalling 384KB total. That’s a savings of 191KB (almost exactly 50%) from a single change to my code. You’d have to remove 19 copies of jQuery 2.0 to achieve this kind of bandwidth savings.

(by the way, didja hear that jQuery 2.0 has small QSA-only custom builds?)

Small screens

Waterfall: Conditional load small

Big screens

Waterfall: Conditional load big

You can see in the second waterfall that the Initiator of big.css is modernizr.min.js, meaning that JavaScript loaded the file after running the test.

ThoughtContentLoaded

I hope this shows how easy it can be to reduce your page weight without worrying about shaving bytes of JavaScript that are supplying valuable functionality if you know how to use them right.

If you want to know more about the conditional loading API within Modernizr, head over to yepnope.js documentation and start reading. For more Drupal-specific examples check out the official documentation for conditional loading using the Modernizr module.

Inlining one-use JavaScript

Everyone does it.

There’s a piece of JavaScript that will only be used on one page, perhaps to provide some unique interactivity. It’s probably attached to a View or maybe a unique node ID. It’s so easy to toss in a drupal_add_js() and move on — or worse, throw the code in your theme. Wouldn’t it be nice if you could inline all these one-use scripts and make them appear only the page they’re needed?

Inline on the Fly

Here’s an easy way to inline scripts without losing the ability to edit them easily. We don’t want our code sitting in a PHP string so we create and maintain a real JS file, and use file_get_contents() to grab it whenever the appropriate page is built.

  // Ensure this JS ends up inline at the bottom of the page
  $options = array(
    'scope' => 'footer',
    'type' => 'inline',
  );
 
  // JS lives in its own file but is included inline when page renders
  $js_code = file_get_contents(drupal_get_path('module','my_module').'/my_code.js');
 
  // Add JS to page
  drupal_add_js($js_code, $options);

Optimized pages + organized code

I often find myself using hook_views_post_build() to apply this behavior when a specific Views display needs some custom JS to function properly. That way I don’t have to worry about the path, it just works anytime this View is used.

Avid Features users know it’s much more maintainable to keep the JS in its own file next to the View instead of stuffing it in a Views footer, or worse: tossing vital code for components into the theme’s “main” (read: only) JS file. Putting code in a theme file can seem swell until you copy a Feature for use in another project and just can’t figure out where that JavaScript went.

/**
 * Implements hook_views_post_build().
 */
function my_feature_views_post_build(&$view) {
  $has_run = &drupal_static(__FUNCTION__);
 
  if (!$has_run) {
    switch ($view->name) {
      // Check for the relevant View(s)
      case 'my_view':
        // Check for the relevant display(s)
        if ($view->current_display == 'my_block') {
          // Ensure this JS ends up inline at the bottom of the page
          $options = array(
          'scope' => 'footer',
          'type' => 'inline',
          );
 
          // JS lives in its own file but is included inline when page renders
          $js_code = file_get_contents(drupal_get_path('module','my_feature').'/my_code.js');
 
          // Add JS to page
          drupal_add_js($js_code, $options);
          $has_run = TRUE;
        }
        break;
    }
  }
}

Performance

Inlining a script avoids an http request and is great for frontend performance. However, if you have a page that is uncached and hit continuously, adding disk reads won’t be so great for the actual server’s performance. You can see in the second example there’s a reference to drupal_static(). This is a good way to avoid running a slow Drupal hook more than once per page request. Always make sure to cache the outcome of functions like this one in order to avoid too many disk reads.

Creating Custom Panels Panes (and use substitution too!)

I spent several hours last night just trying to add some configurable social sharing buttons to my node pane, but I needed to use fields from the node itself within my code. After hours of Google searching, and checking versions– I finally figured out how to do what I was looking for. Part of this confusion is due to just ctools (Chaos Tool Suite) having slightly different API depending on its version. Note, I am using ctools version 7.x-1.2 and panels version 7.x-3.3.

This quick tutorial will show you how to create you own custom panel, the form for settings, and finally how to use the substitution variables from the entity being rendered within your settings.

The .info File and .module File

First off, a custom panels pane is in reality a ctools content type, which has absolutely nothing to do with Drupal’s normal content types. This confusion of verbiage is found throughout a lot of documentation, so to differentiate here, I will only use the word “pane” to reference the ctools content type we are building. To create a pane, you must write your own custom module, although you can use an existing custom module if you want. Per usual, you will need an my_module.info file, and will need to ensure that you have ctools set as a dependency.

name = My Custom Panes
description = Provides several custom panes to panels.
core = 7.x
dependencies[] = ctools

Next, you will need to alert ctools where to look for your custom pane information. This is done with hook_ctools_plugin_directory(), a hook that must be placed within your .module file. This hook will just return a directory path (from your module’s base folder) where ctools should go looking for your plugin files. See the example code below:

/**
 * Implements hook_ctools_plugin_directory().
 */
function mymod_ctools_plugin_directory($owner, $plugin_type) {
  if ($owner == 'ctools' && $plugin_type == 'content_types') {
    return 'plugins/' . $plugin_type;
  }
}

In this example, the hook checks to see if this is for ctools ($owner), then if we are looking for the ‘content_types’ plugins. If you are going to be using more plugins, you can also include them into the ‘if’ statement, however we will not need any other ctools plugins for this example.

Define the Ctools Content Type (Pane)

The last file we will need is the file that will contain the full definition of the panel we want to make. It will live in the folder ‘plugins/content_types’ (taken from the hook return above) within your module directory. The file will have not only the definition, but also the settings form (if needed), and the render function. Do not worry about the name of the file, but ensure it is unique, and name spaced to your module. For this example, the file will be ”mymod_custom_pane.inc’ The first bit is the plugin definition, which will live outside a function, and the first piece of code within your file. Explanations of what each part means are inline.

$plugin = array(
  'single' => TRUE,  // Just do this one, it is needed.
  'title' => t('My Module Custom Pane'),  // Title to show up on the pane screen.
  'description' => t('ER MAH GERD, ERT DERS THINS'), // Description to show up on the pane screen.
  'category' => t('My Module'), // A category to put this under.
  'edit form' => 'mymod_pane_custom_pane_edit_form', // A function that will return the settings form for the pane.
  'render callback' => 'mymod_pane_custom_pane_render', // A function that will return the renderable content.
  'admin info' => 'mymod_pane_custom_pane_admin_info', // A function that will return the information displayed on the admin screen (optional).
  'defaults' => array( // Array of defaults for the settings form.
    'text' => '',
  ),
  'all contexts' => TRUE, // This is NEEDED to be able to use substitution strings in your pane.
);

At this point, once the module is enabled, then you pane should show up when you click to “Add content” to a panel. If it is not showing up, ensure that you do not have pending changes to the panel, as this will prevent updating of panes to occur.

Create a Settings Form

Next, we need to write the functions to create the edit form, render the pane, and create the admin info display. The form is created much like any other form with the FAPI. Your current settings are added to the $form_state variable, and the submit function will automatically saved any settings that are put into the $form_state[‘conf’] array.

/**
 * An edit form for the pane's settings.
 */
function mymod_pane_custom_pane_edit_form($form, &$form_state) {
  $conf = $form_state['conf'];
 
  $form['text'] = array(
    '#type' => 'textfield',
    '#title' => t('Panel Text'),
    '#description' => t('Text to display, it may use substitution strings'),
    '#default_value' => $conf['text'],
  );
 
  return $form;
}
 
/**
 * Submit function, note anything in the formstate[conf] automatically gets saved
 * Notice, the magic that automatically does that for you.
 */
function mymod_pane_custom_pane_edit_form_submit(&$form, &$form_state) {
  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
    if (isset($form_state['values'][$key])) {
      $form_state['conf'][$key] = $form_state['values'][$key];
    }
  }
} 

Render the Pane

Now, we get to (finally!) actually render out content in our pane. For this, we will use the render function we defined above, and ensure that we return our content as a stdClass object with a ‘title’, and ‘content’. This can be either HTML, or renderable arrays, it does not matter. The importatant part here is the function ‘ctools_context_keyword_substitute’, which will take the $contexts variables passed through, and then replace the substitution strings within.

/**
 * Run-time rendering of the body of the block (content type)
 * See ctools_plugin_examples for more advanced info
 */
function mymod_pane_custom_pane_render($subtype, $conf, $args, $contexts) {
 
  // Update the strings to allow contexts.
  if (!empty($contexts)) {
    $content = ctools_context_keyword_substitute($conf['text'], array(), $contexts);
  }
 
  $block = new stdClass();
 
  // initial content is blank
  $block->title = t('This is my title!'); // This will be overridden by the user within the panel options.
  $block->content = $content;
 
  return $block;
}

That’s it! You have made a rendered panel pane. You can of course make this more complex as you needs are. For me, I wrote a custom function that will take the $conf variables, and then create a “tweet this” button. It then had variables for the hashtags, url, and text that should be automatically added to the tweet, but the possibilities are endless.

The Admin Display Page

This last section is optional, but allows you to display information on the panel’s content configuration screen. By default, each pane will just display “No info available.” We have already defined our admin display within the $plugin definition above, so we just need to use the same function name we defined above:

/**
 * 'admin info' callback for panel pane.
 */
function mymod_pane_custom_pane_admin_info($subtype, $conf, $contexts) {
  if (!empty($conf)) {
    $block = new stdClass;
    $block->title = $conf['override_title'] ? $conf['override_title_text'] : '';
    $block->content = $conf['text'];
    return $block;
  }
}

You can, of course, change the $block->content to something more complex, but that is all that is needed for this display.

Other Links

Here are some other tutorials I found very helpful in my search. They provide more examples of some of the functionality here.

I also included the code files used here for you to look at all together. They work, I swear.

And just remember… when in doubt. Clear the cache.

Compass, Aurora, and Corona... Oh my!

Are you a Drupal themer? Do you work with Compass, Susy, Sass? Are you looking to build your next site using the very latest in frontend development tools?

You’ve come to the right blog post then. We’ve been doing our own deep-thinking about themes here at Four Kitchens, and were playing with a lot of new technologies when we came across the Aurora theme developed by Sam “Snugug” Richard. It’s a HTML5, Sass/Compass-powered, mobile-first base theme, and even includes a Compass extension to build your own sub-theme! It handles a lot of features for you — like jQuery Update with local fallback — and all of the templates are up to the very latest standards in HTML5 development.

Project Corona

I’ve also contributed some of the work on our own boilerplate theme into the compass extension, so you can have a great set of tools and the CSS to hit the ground running. We’ve called this project “Corona”, and it’s part of the Aurora compass extension. We’re constantly working on seeing how our Sass partials can be best set up, and will update Corona to reflect those changes.

What makes Corona unique? We’ve created a few mix-ins, variables, font stacks, and settings that we’ve found immensely helpful. These are a compilation of standard tools we find ourselves using on every site, and wanted to have a fast, easy way of starting project with them. We think these tools are helpful enough that everybody should be able to work with them. If you have any suggestions, submit a pull request on the Github repo that houses what the sub-theme should be. We will then convert this into the Aurora compass extension.

How to Implement Aurora / Corona

To install the Aurora compass extension, first ensure you have ruby gems installed, then run the following lines of code:
gem install susy
gem install compass-aurora

If you already have the Aurora compass extension, ensure that you have at least version 1.0.2 by running this command:
gem update compass-aurora

Next, download Aurora put it in your theme directory, then you can create your own sub-theme with Compass. Select one of the two lines below, depending if you want to use Corona (the Four Kitchens sub-theme), or the standard Aurora/Susy sub-theme. Make sure you replace [theme-name] with your own theme name to ensure Compass names all hooks and files appropriately.

Corona
compass create [theme-name] -r aurora --using aurora/corona

Aurora / Susy
compass create [theme-name] -r aurora --using aurora/susy

Helpful Information

There you have it, you have a sub-theme that you can start designing your site with. Sam and I will be updating Aurora frequently as we find new features that should be included. You can always see the current state of the project on Drupal.org. If you have any suggestions, put them up on the project issue queue. Sam has also published a free book within the iTunes store that is very helpful in its documentation of Aurora, check it out for more helpful hints.

Image Credit: NASA on Flickr

Building Custom Blocks with Drupal 7

Introduction

There are times when you need to build a custom block that a site builder can utilize in various places on a page. Drupal 7 provides several hooks that allow you to accomplish this goal:

hook_block_info()
hook_block_configure()
hook_block_save()
hook_block_view()
hook_block_list_alter()
hook_block_view_alter()
hook_block_info_alter()
hook_block_view_MODULE_DELTA_alter()

For this tutorial, we will be using the first four hooks above to build a custom block that will contain WYSIWYG markup and an uploaded picture file. This custom block will be built within a module called custom_block.

Making the Block Selectable

In order to use the block, we have to make it selectable in the block administration menu. To accomplish this, we will use hook_block_info(). Inside this hook, you can assign multiple blocks to your returned array. We will only be adding one block in this tutorial. Add the following code to your module (remember to swap out ‘custom_block’ with the name of your module):

/**
 * Implements hook_block_info().
 */
function custom_block_block_info() {
  $blocks = array();
  $blocks['my_block'] = array(
    'info' => t('My Custom Block'),
  );
 
  return $blocks;
}

After you load this code up in your custom module, you will have the option of selecting and configuring ‘My Custom Block’:


Screenshot of block selection with custom block added.

When you click on configure at this point, you will get the standard block configuration page. We will need additional code in order to enable more block options.

Building the Block Configuration Form

We want to add a WYSIWYG text form for html markup and a file upload widget for our picture file. This can be accomplished within hook_block_configure(). Additionally, we will need two separate form types to pull this off. Inspecting the Form API Reference for Drupal 7, we see that there are two form types that will work perfectly: #text_format and #managed_file.

For the #text_format (WYSIWYG) type form, we need to supply #type, #title, and #default_value. In order for Drupal to remember what you store in this form, you will need to assign a stored variable to #default_value using the variable_get() function.

For the #managed_file (AJAX file upload widget) type form, we need to supply #name, #type, #title, #description, #default_value, #upload_location, and #upload_validators. Again, in order for Drupal to remember the file that we uploaded, #default_value will need to be assigned to the stored file FID variable using the variable_get() function. Additionally, you can specify the subdirectory within your Drupal files directory using #upload_location and allowable file extensions using #upload_validators.

Add the following code to your module:

/**
 * Implements hook_block_configure().
 */
function custom_block_block_configure($delta='') {
  $form = array();
 
  switch($delta) {
    case 'my_block' :
      // Text field form element
      $form['text_body'] = array(
        '#type' => 'text_format',
        '#title' => t('Enter your text here in WYSIWYG format'),
        '#default_value' => variable_get('text_variable', ''),
      );
 
      // File selection form element
      $form['file'] = array(
        '#name' => 'block_image',
        '#type' => 'managed_file',
        '#title' => t('Choose an Image File'),
        '#description' => t('Select an Image for the custom block.  Only *.gif, *.png, *.jpg, and *.jpeg images allowed.'),
        '#default_value' => variable_get('block_image_fid', ''),
        '#upload_location' => 'public://block_image/',
        '#upload_validators' => array(
          'file_validate_extensions' => array('gif png jpg jpeg'),
        ),
      );
      break;
  }
  return $form;
}

Once you’ve added the code to your module, you should have a block configure form that looks like this:


Screenshot of custom block configuration page.

Handling the File Saving

When saving the block, we want to make sure that Drupal will take the necessary steps in order to properly save our text and uploaded file. To accomplish this, we’ll make use of hook_block_save(). This hook is called whenever the ‘Save block’ button is clicked on the block modification form. Two arguments, $delta and $edit, are supplied into hook_block_save(). The $delta variable will contain a string identifier of which block is being configured and $edit will contain an array of submitted block form information.

In order to save our WYSIWYG text, we will use variable_set() function. You’ll need to supply a name and the value as arguments. In our case, the text is captured in the $edit array, specifically within $edit['text_body']['value'].

Saving our uploaded file requires several more steps. First, we need to build a file object for the uploaded file. This is done using the file_load() function and supplying the FID value as the argument. Once the file object is established, the status value needs to be set to FILE_STATUS_PERMANENT. This constant tells Drupal that this file is permanent and should not be deleted. If the file status value is not changed, Drupal assumes that it is temporary and will delete it after the DRUPAL_MAXIMUM_TEMP_FILE_AGE time value is exceeded.

Next, we need to save our file object to the database using the file_save() function. Additionally, we will want to map our block’s usage to the file. To perform this, we first need to build a block object using block_load() while supplying the module name and block name arguments. Then we can set the file usage via the file_usage_add() function. This function passes in the file object, module name, object type associated with the file, and a numeric ID of the object containing the file (in our case, the block BID). Finally, the file FID gets associated with a variable using the variable_set() function, similar to how the text was stored earlier.

Add the following code to your module:

/**
 * Implements hook_block_save().
 */
function custom_block_block_save($delta = '', $edit = array()) {
  switch($delta) {
    case 'my_block' :
      // Saving the WYSIWYG text      
      variable_set('text_variable', $edit['text_body']['value']);
 
      // Saving the file, setting it to a permanent state, setting a FID variable
      $file = file_load($edit['file']);
      $file->status = FILE_STATUS_PERMANENT;
      file_save($file);
      $block = block_load('custom_block', $delta);
      file_usage_add($file, 'custom_block', 'block', $block->bid);
      variable_set('block_image_fid', $file->fid);
      break;
  }
}

After implementing this code, uploading a file, and saving the block, you should have an entry in your file_usage table like this:


Screenshot of file_usage table entry from uploaded file.

Also, there should be an entry in your file_managed table like this:


Screenshot of the file_managed table entry from uploaded file.

Building the Block Output

Now all that is left to do is output the block contents in a meaningful way. To achieve this, we will need to use hook_block_view(). Within this hook all that needs to be done is return a renderable array. To decouple the renderable array construction, we’ll place this logic in a separate function called my_block_view() that simply returns the final renderable array.

Within the my_block_view() function, we retrieve the image file using file_load() and supply it with the stored file FID reference variable. We then check to see if the image file path exists and assign it if present. The image markup is built using theme_image() with an array of attributes getting passed in as the argument. The WYSIWYG text markup variable is retrieved using variable_get(). Finally, the block renderable array is constructed using two sub-arrays ‘image’ and ‘message’.

Add the following code to your module:

/**
 * Implements hook_block_view().
 */
function custom_block_block_view($delta='') {
  $block = array();
 
  switch($delta) {
    case 'my_block' :
      $block['content'] = my_block_view();
      break;
  }
 
  return $block;
}
 
/**
 * Custom function to assemble renderable array for block content.
 * Returns a renderable array with the block content.
 * @return
 *   returns a renderable array of block content.
 */
function my_block_view() {
  $block = array();
 
  // Capture the image file path and form into HTML with attributes
  $image_file = file_load(variable_get('block_image_fid', ''));
  $image_path = '';
 
  if (isset($image_file->uri)) {
    $image_path = $image_file->uri;
  }
 
  $image = theme_image(array(
    'path' => ($image_path),
    'alt' => t('Image description here.'),
    'title' => t('This is our block image.'),
    'attributes' => array('class' => 'class_name'),
  ));
 
  // Capture WYSIWYG text from the variable
  $text = variable_get('text_variable', '');
 
  // Block output in HTML with div wrapper
  $block = array(
    'image' => array(
      '#prefix' => '<div class="class_name">',
      '#type' => 'markup',
      '#markup' => $image,
    ),
    'message' => array(
      '#type' => 'markup',
      '#markup' => $text,
      '#suffix' => '</div>',
    ),
  );
 
  return $block;
}

Now you have all the necessary pieces in place to output the block on your page. Here is the un-styled block output within the ‘Featured’ section:


Screenshot of final custom block output in the featured section of the Drupal site.

Summary

As you have seen, Drupal 7 provides you with several tools to build custom blocks. Using hooks, the Form API, and various storage/retrieval functions, the block construction possibilites are endless. Have suggestions, comments, or better solutions? Let us know!

Training the Web Chef Way

When the Four Kitchens’ team of web chefs develop a new training course, our guiding principle is: Provide a strong return on investment. You invest the time traveling to the training, attending, and afterwards, practicing the skills acquired. You also invest the energy and effort necessary to develop new skills. You place your trust in the trainers to guide you from where you are now to where you need to be. In return, we invest our time, energy, and best effort in creating training experiences that give you a stronger, more relevant, skillset and the confidence you need to apply it.

We also want you and the training to be the right match, building on your current skillset. Before the event, we send a very specific list of required skills, so that you can be certain that the training you purchased is right for you.

To ensure a valuable return on your investment, we develop our trainings with four essentials in mind.

  1. You leave with skills you need. We are interested in many things. The web chefs’ IRC chat room is a steady stream of links and memes. But when it comes to training, we make sure that the skills we teach are the ones you must have as a web professional. We want the skills you develop to increase your value in the marketplace.
  2. Hands-on experience, in class. Seeing is not doing. We know that the only way to develop a skill is to jump in and do it. We provide a safety net. We approach training as an obstacle course designed to build confidence. Instructions are given and then, you tackle the obstacle. We put the smaller obstacles first so that by the end, you are scaling big walls without breaking a sweat.
  3. Subject matter expertise AND training expertise. Many technical training courses fail because the trainers are not subject matter experts or the subject matter experts are not trainers. We develop trainings as a team, combining expertise in the subject with expertise in the art of training. The finished product is an intellectually satisfying, fun, and valuable day with the web chefs.
  4. Enjoyable, cooperative, encouraging. Training is a community experience. We create an environment where trainees can help each other, receive help from us, and participate in every discussion so that the group builds their skills in a cohesive, connected way. We also have a lot of fun.

Our next training is at DrupalCon Munich. Join us for Responsive Websites: Design and Build for All Devices. Also, keep an eye out for more trainings at BadCamp and DrupalCamp Austin.

Do you need personalized training for your team? Contact us for more information about we help teams become Drupal Experts.

DrupalCamp Stanford

Four Kitchens is sponsoring DrupalCamp Stanford and web chef Diana Montalion Dupuis is in sunny Palo Alto to offer two sessions: Drupal for NonGeeks and Mad Skillz: Be the Best in the World.

  • On Friday, attendees who will never write a line of PHP code but need to understand how Drupal works can find out at the Drupal for NonGeeks session. This session will offer a high-level, conceptual understanding of the Drupal framework. The goal is to enable nonGeeks to make decisions about applying Drupal to their real world challenges and talk to developers (in their language).
  • On Saturday, attendees can join in on The Mad Skillz Self Assessment Experience at the Mad Skillz: Be the Best in the World session. They’ll also hear what top Drupal shops and in-house Drupal team leaders say are the “Most Important Traitz” their best developers possess. (Hint: it isn’t “ninja” anything.) Team builders and Drupal business or project owners will get a master list of skillz to use for team development plans, hiring assessments, and ideas for how to assess that “certain something” that top developers have in common.

If you’re at the Camp, come by a session and say, “Howdy.”

Drop that cron; use Hudson instead

For years, I used cron (sometimes anacron) without asking questions. The method was simple enough, and every project requiring cron-related capabilities documented the setup.

There is a much better way, and it involves Hudson Jenkins. I introduced “Hudson for cron” as a sidebar at the Drupal Scalability and Performance Workshop a few weeks ago. To my surprise, several of the attendees remarked on their feedback questionnaires that it was one of the most valuable things they picked up that day. So, I’ve decided to write this up for everyone.

Why not cron?

First, I have the burden of explaining why you should drop most use of the tried-and-true cron. To be honest, I don’t think cron is even a “good enough” solution for most of today’s systems:

  • You either get email from every run’s output, a dumb log to disk, or no reporting at all. When you do log to disk, you have to worry about segmenting and rotating logs.
  • Jobs have to be manually staggered to avoid massive slowdowns every hour (or midnight or ten minutes or other interval)
  • Even if the previous job hasn’t finished, cron happily starts up a new one on top of it
  • It doesn’t integrate with any other job kickoff or monitoring system
  • There’s no built-in ability to run remote jobs, let alone move a remote job from one machine to another
  • Most of the web-based tools for configuring cron aren’t very nice
  • There’s no built-in logging of job execution time, even though a cron job taking excessive time is one of the most common failure cases.

Why Hudson is better

Here’s how using Hudson with periodic “builds” beats cron:

  • Among the myriad ways Hudson can measure success of a “build,” it can verify a zero return status from each “execute shell” build step. If a job simply returns anything but zero, Hudson considers the build a failure and can notify you however you like. It can email you (on first failure only or every time), you can subscribe to build feeds via RSS, or you can simply use the Hudson interface as a dashboard that shows failures in a convenient, summarized way.
  • Hudson logs the output of “execute shell” build steps. Success or failure, Hudson archives the build output without filling your inbox or local disk. If the console output isn’t enough, Hudson can archive per-run “build artifacts,” which are files on disk matching a defined pattern. There’s also no-hassle “log rotation” by specifying a cap on the number of builds or a set number of days to keep results; this is configurable per-job. If a particular run had output (say, for troubleshooting) you want to keep around, you can tell Hudson to “keep this build” indefinitely.
  • Hudson runs each build on “build executors,” which are effectively process slots. Any system can have any number, but it puts a cap on how much Hudson tries to do, systemwide. This mean 50 jobs can get scheduled to run every hour with four “build executors,” and Hudson will queue them all every hour and run four at once until they’ve all finished.
  • If a job is still running when the “periodic build” time comes around, Hudson can either run the job immediately (like cron) or queue the job to run when the one in progress finishes.
  • Hudson isn’t limited to time-based scheduling. Sometimes, it’s useful to take a job that used to run periodically (say, a database refresh) and make it only available for manual kickoff. Of course, as a CI tool, Hudson can kick off jobs based on polling a version-control system.
  • For remote jobs, Hudson can sign onto systems with SSH, copy over its own runtime, and run whatever you’d like on the remote system. This means that, no matter how many servers in a cluster need scheduled jobs, Hudson can schedule, run, and log them from one server. Hudson can distribute the jobs dynamically based on which machines are already busy, or it can bind jobs to specific boxes.
  • Hudson has a solid web interface that can integrate with your Unix shadow file, LDAP, or other authentication methods. For people who prefer operating from the command line, Hudson has a CLI.
  • Every job’s running time is logged. Hudson even provides estimates for how long it will take the system to get to any particular job when there’s a queue.

Hudson isn’t perfect

To be fair, there are still a couple reasons continue using cron:

  • As a Java-based web application, Hudson is heavyweight. A low-memory or embedded system is better-off with cron. Even Hudson’s remote job invocation installs and starts a Java-based runtime. You can, however, use the SSH plugin if even one box can run the main Hudson instance.
  • Cron’s scheduling is more precise if things have to happen exactly at certain time intervals. Hudson’s assumption is that your periodic builds aren’t dependent on when they start within a minute or two.

Adding a cron-style job to Hudson

Moving jobs from cron to Hudson is easy:

  1. Install Hudson. From the front page of Hudson’s site, there are repositories for Red Hat Enterprise Linux, CentOS, Ubuntu, Debian, and a few others.
  2. Open Hudson in a browser (on port 8080 by default).
  3. Add a new “New Job” of type “Build a free-style software project.”
  4. Check “Build periodically” and put in a cron-like schedule.
  5. Click “Add build step” and “Execute shell.” The Hudson wiki has a page explaining this.
  6. Configure access control from within Hudson.

Drupal and Pressflow best practices

It’s easy to drop in the standard call to wget or cURL to run your Drupal/Pressflow cron from Hudson, but the best way is with Drush.

Why Hudson with Drush?

  • You can configure PHP’s CLI mode to be liberal in error reporting, giving you far more data on failure than a WSOD from wget or cURL. Hudson will also fail the “build” if PHP runs into a fatal error.
  • You can block access to cron.php entirely. (This advantage isn’t unique to Hudson integration.)

Because Drush requires local shell execution, there’s a bit more overhead to having one Hudson box run Drupal’s cron on remote servers in a cluster. It’s not that hard, though. Just configure a Hudson “slave” on each box that needs to run Drush and configure each job to run on the “build executor” that hosts the site. If using a Hudson slave is overkill, use the SSH plugin.

There are even better reasons to use Drush with Hudson for things like database schema updates, but that’s outside the scope of this blog entry.

In the wild

Four Kitchens is widely using Hudson for cron automation on client sites. We’ve also deployed Hudson to Drupal.org infrastructure for multiple non-testing purposes, including deploying updates to Drupal.org (to be discussed in a future blog post).

Pages