Jul 11 2018
Jul 11

Coffee is a magical thing. It gets you going, clarifies your thoughts, makes you feel all warm inside. I don’t know what I’d do without it. So when we consider installing the Drupal module named after this irreplaceable daily beverage, we see that it has a similar effect. It just makes things better. Am I overstating things? Probably. But I haven’t had enough coffee yet today and I need to get this blog going with some pizzazz.

So simply put, the Coffee module gives you a Mac-like live search experience of the Drupal admin menu. It is triggered by the shortcut of Alt+D on the Mac, or Alt+ctrl+D in Windows IE. When the search bar pops up, just start typing and you can do an ajax live search of that deeply nested admin menu and get to things in a hurry! 

Screenshot of Coffee Drupal Module

Now, it’s not perfect. It can’t dig beyond certain levels, such as actually finding the settings of individual contact forms, but it gets nearly everywhere in the first couple of layers and can save you a lot of clicks when in development and site building mode. Even cooler - you can add your own custom coffee commands in your custom module to make your actions discoverable. That’s great flexibility. 

So there you have it! A simple module that shaves off a few seconds of time all day long. Pretty awesome!

Time to refill my mug.

Sep 19 2017
Sep 19

The holidays are over for a while now, so it’s about time for a new blog. In this article I’ll discuss 12 modules that can help you get started with a great Drupal site:

1. Max image size

A default Drupal installation can check if an uploaded image is too large and display a warning. This module does something similar but is also checking previously uploaded images that are too large and likely taking up too much space.
 It scans all the images (also already uploaded ones) and reduces the size of the original

More info and download — Drupal 7

2. User Import

Useful module to import users using a CSV file.

More info and download — Drupal 7

3. Select (or other)

Drupal’s form API knows by default a select element that allows you to offer choices to those who enter content. This element is limited to the provision of predefined terms (categories). After installing this module, this element can be expanded with an additional field: let the end user choose ‘other’ and offer a free selection field.

More info and download — Drupal 7 & Drupal 8 Alpha

4. Captcha-free Form Protection

Everybody wants to be protected against spammers, this is often done through the Captcha technology; probably you have heard of this before. This module protects you against spammers without Captcha, since this is often a barrier for visitors.

The module applies other techniques (‘behind the scenes’) such as checks if cookies / javascript are disabled, it can also check whether a certain time has exceeded. On the basis of these data it can determine whether the person who sent a form is most likely a spammer or not. The Honeypot module contains similar end features.

More info and download — Drupal 7 and Drupal 8

5. Twitter block

Simple but common used module: shows a Twitter stream from a particular account.

More info and download — Drupal 7 and Drupal 8

6. Leaflet

Leaflet is a javascript library that is quickly becoming popular and that let’s you create maps. It is an alternative to Google maps, allowing you to easily create customized maps and integrate external map services (for example Mapbox, Stamen or Thunderforest). Easy to configure, mobile-friendly to navigate and light in code.

For a detailed introduction see Drupalize.me.

More info and download — Drupal 7 & Drupal 8 dev

7. Better watchdog UI

The Drupal core has a logging module which gives great insights in errors, notices, content and user actions, etc. Install this module if you want to filter better in this log.
 FYI: Till Drupal 5 Drupal’s logging module was called ‘watchdog’, this term is still used for logging elements.

More info and download — Drupal 7

8. Check for acknowledgement

In some cases you want to know whether users of your system have read a particular piece of content. This is now possible after installing this module: it places a check mark at the bottom of a content page. Users placing the check mark are logged which is visible to you as a site administrator. This allows you to see who really confirmed they read the article.

More info and download — Drupal 7

9. IP address manager

Log the IP addresses of users logging into your Drupal site. This can be used for many things:

  • Detecting suspicious logins;
  • Identifying misconduct;
  • Detecting duplicate accounts.

More info and download — Drupal 7

10. Taxonomy container

Make the choice easier for content managers by clustering terms better.

More info and download — Drupal 7 & Drupal 8 beta

11. Date Facets

A widget for when you are using the Facet API: generates an additional block in which date-based filtering options are offered.

More info and download — Drupal 7

12. Read only mode

When putting your system in the maintenance mode in a default Drupal installation, the entire system will be temporarily put offline; the visitors will receive a maintenance message. Usually you would prefer that nobody is logged in on your site, as content can be changed during the update process. Those changes could be lost.

If you can ensure that nobody can enter/change content during maintenance, then you are also adequately covered — provided that your update is not generating errors. This module is doing just that: it places your site in the maintenance mode, so visitors can still view the site but cannot enter/change content.

More info and download — Drupal 7 & Drupal 8 beta

Wrap up

And finally, I discovered this cool site: modulecharts.org . Next month again a module update, so stay tuned! Questions or feedback? Let me know in the comments below

Sep 18 2017
Sep 18

So…. Drupal 8 got released! Congrats everybody! The end of life of Drupal 6 is then final. In addition, the 16th of november it was announced that Drupal.org is now serving its content and files via Fastly; which is giving a significant performance boost, well done!

Furthermore, what I noticed last month on module updates:

1) Scroll to destination anchors

This module modifies the behaviour of an ‘anchor’ within a page. So the page will not jump down but fluently scroll down. We have installed this module here.

https://www.drupal.org/project/scroll_to_destination_anchors

2) Spider Slap

There are a lot of ‘evil spiders’ active on the internet. These are web crawlers that don’t respect what is written in your robots.txt. This can cause unnecessary load on your server and uncover information that you don’t want to see in a search engine.
 This module is solving this problem. It will block the IP when a spider does not behave, with as a result that it will no longer have access.

https://www.drupal.org/project/spiderslap

3) Bounce Convert

Do you want to make an announcement in the last moment before a visitor is closing your Drupal website? Then this module can be useful. It functions like Exit monitor or Bounce Exchange.

Introduction video.

!) Note that it is currently an alpha module, so not yet suitable for live Drupal sites.

https://www.drupal.org/project/bounce_convert

4) Database Email Encryption

Do you want to maximise the security of the email addresses of your registered users? This is possible with this module. It encrypts the addresses in the database. Should the database end up in the wrong hands, then the email addresses cannot be read. Encryption is done using AES.

https://www.drupal.org/project/dbee

5) Unique field

A popular module existing since Drupal 5, but I never noticed it before. It is performing a check on entered fields (e.g. title field) and checks whether the entered title is unique. This will prevent the use of double titles which is good for, among others, SEO.

6) Login History

By default Drupal is not creating a login archive. This module will do this for you: it creates an archive in which the history of logins will be stored.

https://www.drupal.org/project/login_history Similar:

https://www.drupal.org/project/login_tracker

https://www.drupal.org/project/login_activity

7) Sitemap

Generates a sitemap for your Drupal 8 website and can also create RSS feeds for example for your blog. This is the Drupal 8 version for the popular Drupal 7 module Sitemap.

https://www.drupal.org/project/sitemap

8) D8 Editor File Upload

Easily place files in content. This Drupal 8 module adds a new button to the editor, which will make it easy to upload and place files.

https://www.drupal.org/project/editor_file

9) Client side Validation

Validating a form in your Drupal website without refreshing the page. This widely used module now offers a Release Candidate for Drupal 8.

https://www.drupal.org/project/clientside_validation

10) App Link

Probably you recognize this one: the message above a website on your Smartphone that you can view the page in a native app. If you built and linked an app (e.g. via DrupalGap) then you can generate this ‘app message’ on your Drupal website using this module.

https://www.drupal.org/project/app_link

11) OpenLucius News

An own module that has to be mentioned;). This module extends Drupal social intranet OpenLucius with a ‘news tab’ on the homepage. News about the organization can be placed here.

https://www.drupal.org/project/openlucius_news

12) Simple XML sitemap

The title of this Drupal 8 module says it all: it provides an XML sitemap that you can upload to search engines, so you can see the indexation of all your site links in the relevant search engine.
 The module also has a few configuration options like setting ‘priority’.

https://www.drupal.org/project/simple_sitemap

13) Session Limit

Tighten the security of your Drupal system by limiting the number of sessions with which a user is logged in. You can for example set that somebody can be logged in once; if somebody is logging in on his/her Smartphone then he/she will be automatically logged out on the work computer.

https://www.drupal.org/project/session_limit

14) Login Security

Provide additional security when logging in, it is for example possible to:

  • Set how many times a user can attempt to log in before the account is blocked.
  • Deny access based on IP, temporarily or permanently.

The module can also send emails (or a log to Nagios) to alert the Drupal administrator that something is going on:

  • It seems that passwords and accounts are guessed.
  • bruteforce attacks or other inappropriate behaviour when logging in.

https://www.drupal.org/project/login_security

15) OpenLucius LDAP

Another own module, that should be mentioned as well ;-). This module extends Drupal social intranet OpenLucius with a LDAP connection, so that users can login to OpenLucius with their existing LDAP account.

https://www.drupal.org/project/openlucius_ldap

16) Protected node

Gives additional protection to a certain page (node). A password can be set when creating the node. If somebody then wants to look at the node, the password must be entered to get access.

https://www.drupal.org/project/protected_node

17) Code per Node

It is common to ‘deploy’ Drupal code (PHP, JS, CSS) via GIT within an OTAP street to a live Drupal server. Usually with the use of a Continuous Integration tool.

[edit] As Eelke mentioned in the comments below: OTAP has to be DTAP for English audience. [/edit]

But with this module you can perform quick fixes per page without the whole operation. It offers the opportunity to add additional CSS; per node, content type, block or global.

Not how we would do it, but I can imagine that this could be a handy tool for Drupal site builders. This is probably also the reason why it is so popular.

https://www.drupal.org/project/cpn

18) Admin Toolbar

A handy toolbar for Drupal 8

https://www.drupal.org/project/admin_toolbar

Wrap up

Alright, these are the modules for this month. In December we will introduce again new ‘cool Drupal modules’, so stay tuned!

Jul 06 2016
Jul 06

Google Summer of Code (GSoC) is into the next phase of coding after the mid-Term evaluations which got over by June 27th. This also reminds students to speed up the coding activities to complete the projects within the schedules provided in the proposal.

I am porting Search Configuration module to Drupal 8 as part of this year’s summer of code. GSoC is definitely turning out to be a venue for the young students from universities around the world to work on real-world projects under the experience of well-known developers, learning new technologies, making use of version control systems, with regular meetings and finally building up a software which is going to help a large section of the society.

I blog regularly, sharing my project progress. If you would like to have a glimpse of my past activities on this port, please visit this link.

Drupal 8 has introduced the concept of Html twigs in place of the PHP templates. So, the PHP template files have to be now ported to the Html environment. The .tpl.php template file is replaced by the .html.twig file for the module templates.  Templates are simply text files which can give outputs in Html, Latex, CSV or XML formats.

To print some data, we usually take the service of echo statements in PHP.  The print statements are replaced by {{ }} in Html twigs.

<?php echo t(‘Let’s start the process.’); ?>

is replaced by:

{{ ‘Le’s start the process’|t }}

The variable names have to be converted to simple names. For instance,

$page[‘title’]

becomes

{{  title }}

The PHP logics have to be replaced by {% %} syntax. This is applicable to all the logical statements.

<?php if ($page[‘title]): ?>

…..

<?php endif; ?>

is transformed as:

{% if form %}

……

{% endif %}

Also, the variables are replaced by simple names.

<?php if ($logo): ?>

is transformed as:

{% if logo %}

These were some of the basic transformations to get started into created the HTML twigs.The use of the Html twigs has made the templates look very simple and easily understandable. It is really easy to get the templates converted to the Html twigs. This is always one of the crucial requirements of porting modules from Drupal 7 to Drupal 8.

Stay tuned for further updates on this port process.

Jun 21 2016
Jun 21

Google Summer of Code (GSoC), has entered into the mid-Term evaluation stage. This is a 1 week period from 21- 27 June, were students and mentors present the progress of their projects. Based on the reports submitted, students are made pass/ fail.

I have been working on porting Search Configuration to Drupal 8 in the past few weeks. If you would like to have a quick glimpse of my past activities on this port process, please go through these posts.

last week, I could learn some Drupal concepts which were really helpful for my project. In the previous versions of Drupal, the role permissions were stored in a role_permissions table in the Database. But now, in Drupal 8, the role permissions are directly stored in the role configuration entity.

So, as described above, in D7 and its preceding versions, role permissions were stored in a role_permissions database which had the role Id and the corresponding permissions. The permissions distributed to a role was retrieved in D7 using:

$permissions = role->getPermissions();

But, in D8, this is done by the

$permissions = role->getPermissions();

Another instance is that, to grant certain permissions to roles.

In D7 it was controlled by,

user_role_grant_permissions($rid, array(‘ access content’));

The role configuration entity remodels this functionality in D8 to:

$role->grantPermission(‘ access content’);

In connection with the term permissions, the most important aspect in Drupal is a hook: hook_permissions(). This hook, obviously as you might have guessed, distributes the permissions to various users; decides whether a particular user should be allowed to access a page or a content, granting and restricting the access.

This hook has been replaced in Drupal 8 by a module.permissions.yml file. This file contains the permissions and its specifications. We can write a driver function in a php file to add the dynamic permissions. This can be achieved by making a driver class in the php file and adding the behaviour of the permission we need in the member functions of the class. We also have to link this PHP file with our yml file to keep it active. This is done by adding a callback function in the yml file which references this php file.

To display special characters in a plain text string for display as HTML format, Drupal earlier versions used the function check_plain.  This had the general syntax:

check_plain($text); // where $text was the string to be processed.

This function has got deprecated in Drupal 8. This has been replaced by the \Drupal\Compoent\Utility\Html::escape($text).

May 20 2016
May 20

I have been selected for the Google summer of Code’ 16 for Drupal for the project, Port search configuration module to Drupal 8. Thanks to all the developers in #Drupal IRC channel for guiding me into this summer project by sharing their ideas and suggestions.

The search configuration feature is presently available in Drupal 7 and its preceding versions. This is really a cool feature which helps us a lot in improving the search and enhancing it for better search results. This summer, I will be engaged in porting this module to Drupal 8.

The GSoC projects were announced on April 22, 2016. All selected students have a community bonding period till May 22nd. This is the time when students get closer to the organisation, learn the organisation code base, interact with mentors, plan more about the project and its deadline for the coding period which starts soon  after this community bonding.

I have been blessed with three experienced mentors from Drupal- Naveen Valecha, Neetu Morwani and Karthik Kumar. I have been discussing with them regarding the project plan for the last few weeks. Meanwhile, I was also asked to learn some of the basic concepts of Drupal like hooks, hook permissions, forms in Drupal which are the real components of my project. This helped me a lot to understand more about the coding methodologies I need to adopt.  I could go through the code base of the module in Drupal 7 which has helped me collect more ideas for the project.

I also got the opportunity to hack with some simple modules by creating a sandbox project in Drupal and pushing commits on sample module developments I did to learn the basics of the module. I have created a project in Drupal for the search configuration port and has added the tasks I need to complete in association with this process.

I  will be posting regular updates regarding my GSoC project here.

Best of luck to all the selected students.

Looking for a bright summer ahead, coding for Drupal.

Thank you.

Nov 17 2015
Nov 17

We just launched our first Drupal 8 website for the Northwest Atlantic Marine Alliance (NAMA). During our project retrospective, a few of us brought up how nice it was that so many contrib modules that were part of the D6 site weren’t necessary in Drupal 8 – this was a major factor in making it possible to launch this project before the actual D8 release.

The graphic below compares contrib modules that were running on NAMA’s Drupal 6 site compared to the modules needed to achieve the same functionality in Drupal 8.

Having so many of the modules that we always install built into core gave us more time to focus on ways to optimize the site for NAMA’s specific needs, and it should also save us time down the road when it’s time to handle maintenance tasks (like security updates) or add on new features.

What are you most excited about having in core?

Which modules are you waiting to see in D8 before upgrading?

Feb 26 2015
Feb 26

This is the third in a series of blog posts about the relationship between Drupal and Backdrop CMS, a recently-released fork of Drupal. The goal of the series is to explain how a module (or theme) developer can take a Drupal project they currently maintain and support it for Backdrop as well, while keeping duplicate work to a minimum.

  • In part 1, I introduced the series and showed how for some modules, the exact same code can be used with both Drupal and Backdrop.
  • In part 2, I showed what to do when you want to port a Drupal module to a separate Backdrop version and get it up and running on GitHub.
  • In part 3 (this post), I'll wrap up the series by explaining how to link the Backdrop module to the Drupal.org version and maintain them simultaneously.

Linking the Backdrop Module to the Drupal.org Version and Maintaining Them Simultaneously

In part 2 I took a small Drupal module that I maintain (User Cancel Password Confirm) and ported it to Backdrop. In the end, I wound up with two codebases for the same module, one on Drupal.org for Drupal 7, and one on GitHub for Backdrop.

However, the two codebases are extremely similar. When I fix a bug or add a feature to the Drupal 7 version, it's very likely that I'll want to make the exact same change (or at least an extremely similar one) to the Backdrop version. Wouldn't it be nice if there were a way to pull in changes automatically without having to do everything twice manually?

If you're a fairly experienced Git user, you might already know that the answer is "yes". But if you're not, the process isn't necessarily straightforward, so I'm going to document it step by step here.

Overall, what we're doing is simply taking advantage of the fact that when we imported the Drupal.org repository into GitHub in part 2, we pulled in the entire history of the repository, including all of the Drupal commits. Because our Backdrop repository knows about these existing commits, it can also figure out what's different and pull in the new ones when we ask it to.

In what follows, I'm assuming a workflow where changes are made to the Drupal.org version of the module and pulled into Backdrop later. However, it should be relatively straightforward to reverse these instructions to do it the other way around (or even possible, but perhaps less straightforward, to have a setup where you can do it in either direction).

  1. To start off, we need to make our local clone of the Backdrop repository know about the Drupal.org repository. (A local clone is obtained simply by getting the "clone URL" from the GitHub project page and copying it locally, for example with the command shown below.)
    git clone [email protected]:backdrop-contrib/user_cancel_password_confirm.git
    

    First let's check what remote repositories it knows about already:

    $ git remote -v
    origin    [email protected]:backdrop-contrib/user_cancel_password_confirm.git (fetch)
    origin    [email protected]:backdrop-contrib/user_cancel_password_confirm.git (push)
    

    No surprise there; it knows about the GitHub version of the repository (the "origin" repository that it was cloned from).

    Let's add the Drupal.org repository to this list and check again:

    $ git remote add drupal http://git.drupal.org/project/user_cancel_password_confirm.git
    $ git remote -v
    drupal    http://git.drupal.org/project/user_cancel_password_confirm.git (fetch)
    drupal    http://git.drupal.org/project/user_cancel_password_confirm.git (push)
    origin    [email protected]:backdrop-contrib/user_cancel_password_confirm.git (fetch)
    origin    [email protected]:backdrop-contrib/user_cancel_password_confirm.git (push)
    

    The URL I used here is the same one I used in part 2 to import the repository to GitHub (that is, it's the public-facing Git URL of my project on Drupal.org, available from the "Version control" tab of the drupal.org project page, after unchecking the "Maintainer" checkbox - if it’s present - so that the public URL is displayed). I've also chosen to give this repository the name "drupal". (Usually the convention is to use "upstream" for something like this, but in GitHub-land "upstream" is often used in a slightly different context involving development forks of one GitHub repository to another. So for clarity, I'm using "drupal" here. You can use anything you want to.)

  2. Next let's pull in everything from the remote Drupal repository to our local machine:
    $ git fetch drupal
    remote: Counting objects: 4, done.
    remote: Compressing objects: 100% (2/2), done.
    remote: Total 3 (delta 0), reused 0 (delta 0)
    Unpacking objects: 100% (3/3), done.
    From http://git.drupal.org/project/user_cancel_password_confirm
    * [new branch]          7.x-1.x -> drupal/7.x-1.x
    * [new branch]          master  -> drupal/master
    * [new tag]             7.x-1.0-rc1 -> 7.x-1.0-rc1
    

    You can see it has all the branches and tags that were discussed in part 2 of this series. However, although I pulled the changes in, they are completely separate from my Backdrop code (the Backdrop code lives in "origin" and the Drupal code lives in "drupal").

    If you want to see a record of all changes that were made to port the module to Backdrop at this point, you could run git diff drupal/7.x-1.x..origin/1.x-1.x to examine them.

  3. Now let's fix a bug on the Drupal.org version of the module. I decided to do a simple documentation fix: Fix documentation of form API functions to match coding standards

    I made the code changes on my local checkout of the Drupal version of the module (which I keep in a separate location on my local machine, specifically inside the sites/all/modules directory of a copy of Drupal so I can test any changes there), then committed and pushed them to Drupal.org as normal.

  4. Back in my Backdrop environment, I can pull those changes in to the "drupal" remote and examine them using git log:
    $ git fetch drupal
    remote: Counting objects: 5, done.
    remote: Compressing objects: 100% (3/3), done.
    remote: Total 3 (delta 2), reused 0 (delta 0)
    Unpacking objects: 100% (3/3), done.
    From http://git.drupal.org/project/user_cancel_password_confirm
      7a70138..997d82d  7.x-1.x     -> drupal/7.x-1.x
    
    $ git log origin/1.x-1.x..drupal/7.x-1.x
    commit 997d82dce1a4269a9cee32d3f6b2ec2b90a80b33
    Author: David Rothstein 
    Date:   Tue Jan 27 13:30:00 2015 -0500
    
            Issue #2415223: Fix documentation of form API functions to match coding standards.
    

    Sure enough, this is telling me that there is one commit on the Drupal 7.x-1.x version of the module that is not yet on the Backdrop 1.x-1.x version.

  5. Now it's time to merge those changes to Backdrop. We could just merge the changes directly and push them to GitHub and be completely done, but I'll follow best practice here and do it on a dedicated branch with a pull request. (In reality, I might be doing this for a more complicated change than a simple documentation fix, or perhaps with a series of Drupal changes all at once rather than a single one. So I might want to formally review the Drupal changes before accepting them into Backdrop.)

    By convention I'm going to use a branch name ("drupal-2415223") based on the Drupal.org issue number:

    $ git checkout 1.x-1.x
    Switched to branch '1.x-1.x'
    
    $ git checkout -b drupal-2415223
    Switched to a new branch 'drupal-2415223'
    
    $ git push -u origin drupal-2415223
    Total 0 (delta 0), reused 0 (delta 0)
    To [email protected]:backdrop-contrib/user_cancel_password_confirm.git
    * [new branch]          drupal-2415223 -> drupal-2415223
    Branch drupal-2415223 set up to track remote branch drupal-2415223 from origin.
    
    $ git merge drupal/7.x-1.x
    Auto-merging user_cancel_password_confirm.module
    Merge made by the 'recursive' strategy.
    user_cancel_password_confirm.module |   10 ++++++++--
    1 file changed, 8 insertions(+), 2 deletions(-)
    

    In this case, the merge was simple and worked cleanly. Of course, there might be merge conflicts here or other changes that need to be made. You can do those at this time, and then git push to push the changes up to GitHub.

  6. Once the changes are pushed, I went ahead and created a pull request via the GitHub user interface, with a link to the Drupal.org issue for future reference (I could have created a corresponding issue in the project's GitHub issue tracker also, but didn't bother):
    • Fix documentation of form API functions to match coding standards (pull request) (diff)

    Merging this pull request via the GitHub user interface gets it onto the official 1.x-1.x Backdrop branch, and into the Backdrop version of the module.

    Here's the commit for Drupal, and the same one for Backdrop:

    http://cgit.drupalcode.org/user_cancel_password_confirm/commit/?id=997d8...
    https://github.com/backdrop-contrib/user_cancel_password_confirm/commit/...

Using the above technique, it's possible to have one main issue (in this case on Drupal.org) for any change you want to make to the module, do essentially all the work there, and then easily and quickly merge that change into the Backdrop version without the hassle of repeating lots of manual, error-prone steps.

Hopefully this technique will be useful to developers who want to contribute their work to Backdrop while also continuing their contributions to Drupal, and will help the two communities continue to work together. Thanks for reading!

Further Backdrop Resources

Do you have any thoughts or questions, or experiences of your own trying to port a module to Backdrop? Leave them in the comments.

Feb 17 2015
Feb 17

This is the second in a series of blog posts about the relationship between Drupal and Backdrop CMS, a recently-released fork of Drupal. The goal of the series is to explain how a module (or theme) developer can take a Drupal project they currently maintain and support it for Backdrop as well, while keeping duplicate work to a minimum.

  • In part 1, I introduced the series and showed how for some modules, the exact same code can be used with both Drupal and Backdrop.
  • In part 2 (this post), I'll explain what to do when you want to port a Drupal module to a separate Backdrop version and get it up and running on GitHub.
  • In part 3, I'll explain how to link the Backdrop module to the Drupal.org version and maintain them simultaneously.

Porting a Drupal Module to Backdrop and Getting it Up and Running on GitHub

For this post I’ll be looking at User Cancel Password Confirm, a very small Drupal 7 module I wrote for a client a couple years back to allow users who are canceling their accounts to confirm the cancellation by typing in their password rather than having to go to their email and click on a confirmation link there.

We learned in part 1 that adding a backdrop = 1.x line to a module’s .info file is the first (and sometimes only) step required to get it working with Backdrop. In this case, however, adding this line to the .info file was not enough. When I tried to use the module with Backdrop I got a fatal error about a failure to open the required includes/password.inc file. What's happening here is simply that Backdrop (borrowing a change that's also in Drupal 8) reorganized the core directory structure compared to Drupal 7 to put most core files in a directory called "core". When my module tries to load the includes/password.inc file, it needs to load it from core/includes/password.inc in Backdrop instead.

This is a simple enough change that I could just put a conditional statement into the Drupal code so that it loads the correct file in either case. However, over the long run this would get unwieldy. Furthermore, if I had chosen a more complicated module to port, one which used Drupal 7's variable or block systems (superseded by the configuration management and layout systems in Backdrop) it is likely I'd have more significant changes to make.

So, this seemed like a good opportunity to go through the official process for porting my module to Backdrop.

Backdrop contrib modules, like Backdrop core, are currently hosted on GitHub. Regardless of whether you're already familiar with GitHub from other projects, there are some steps you should follow that might not be familiar, to make sure your Backdrop module's repository is set up properly and ultimately to get it included on the official list of Backdrop contributed projects.

Importing to GitHub

The best way to get a Drupal module into GitHub is to import it; this preserves the pre-Backdrop commit history which becomes important later on.

Before you do this step, if you're planning to port a Drupal module that you don't maintain, it's considered best practice to notify the current maintainer and see if they'd like to participate or lead the Backdrop development themselves (see the "Communicating" section of the Drupal 7 to Backdrop conversion documentation for more information). In my case I'm already the module maintainer, so I went ahead and started the import:

  1. Go to the GitHub import page and provide the public URL of the Drupal project's Git repository (which I got from going to the project page on Drupal.org, clicking the "Version control" tab, and then - assuming you are importing a module that you maintain - making sure to uncheck the "Maintainer" checkbox so that the public URL is displayed). Drupal.org gives me this example code:


    git clone --branch 7.x-1.x http://git.drupal.org/project/user_cancel_password_confirm.git

    So I just grabbed the URL from that.

  2. Where GitHub asks for the project name, use the same short name (in this case "user_cancel_password_confirm") that the Drupal project uses.
  3. Import the project into your own GitHub account for starters (unless you're already a member of the Backdrop Contrib team - more on that later).

Here's what it looks like:
GitHub import

Submitting this form resulted in a new GitHub repository for my project at https://github.com/DavidRothstein/user_cancel_password_confirm.

As a final step, I edited the description of the GitHub project to match the description from the module's .info file ("Allows users to cancel their accounts with password confirmation rather than e-mail confirmation").

Cleaning Up Branches and Tags

Next up is some housekeeping. First, I cloned a copy of the new repository to my local machine and then used git branch -r to take a look around:


$ git clone [email protected]:DavidRothstein/user_cancel_password_confirm.git
$ git branch -r
origin/7.x-1.x
origin/HEAD -> origin/master
origin/master

Like many Drupal 7 contrib projects, this has a 7.x-1.x branch where all the work is done and a master branch that isn't used. When I imported the repository to GitHub it inherited those branches. However, for Backdrop I want to do all work on a 1.x-1.x branch (where the first "1.x" refers to compatibility with Backdrop core 1.x).

  1. So let's rename the 7.x-1.x branch:


    $ git checkout 7.x-1.x
    Branch 7.x-1.x set up to track remote branch 7.x-1.x from origin.
    Switched to a new branch '7.x-1.x'
    $ git branch -m 7.x-1.x 1.x-1.x
    $ git push --set-upstream origin 1.x-1.x
    Total 0 (delta 0), reused 0 (delta 0)
    To [email protected]:DavidRothstein/user_cancel_password_confirm.git
    * [new branch] 1.x-1.x -> 1.x-1.x
    Branch 1.x-1.x set up to track remote branch 1.x-1.x from origin.

  2. And delete the old one from GitHub:


    $ git push origin :7.x-1.x
    To [email protected]:DavidRothstein/user_cancel_password_confirm.git
    - [deleted] 7.x-1.x

  3. We want to delete the master branch also, but can't do it right away since GitHub treats that as the default and doesn't let you delete the default branch.

    So I went to the module's GitHub project page, where (as the repository owner) I have a "Settings" link in the right column; via that link it's possible to change the default branch to 1.x-1.x through the user interface.

    Now back on my own computer I can delete the master branch:


    $ git push origin :master
    To [email protected]:DavidRothstein/user_cancel_password_confirm.git
    - [deleted] master

  4. On Drupal.org, this module has a 7.x-1.0-rc1 release, which was automatically imported to GitHub. This won't be useful to Backdrop users, so I followed the GitHub instructions for deleting it.
  5. Finally, let's get our local working copy somewhat in sync with the changes on the server. The cleanest way to do this is probably just to re-clone the repository, but you could also run git remote set-head origin 1.x-1.x to make sure your local copy is working off the same default branch.

The end result is:


$ git branch -r
origin/1.x-1.x
origin/HEAD -> origin/1.x-1.x

Just what we wanted, a single 1.x-1.x branch which is the default (and which was copied from the 7.x-1.x branch on Drupal.org and therefore contains all its history).

Updating the Code for Backdrop

Now that the code is on GitHub, it's time to make it Backdrop-compatible.

To do this quickly, you can just make commits to your local 1.x-1.x branch and push them straight up to the server. In what follows, though, I'll follow best practices and create a dedicated branch for each change (so I can create a corresponding issue and pull request on GitHub). For example:


$ git checkout -b backdrop-compatibility
$ git push -u origin backdrop-compatibility

Then make commits to that branch, push them to GitHub, and create a pull request to merge it into 1.x-1.x.

  1. To get the module basically working, I'll make the simple changes discussed earlier:
    • Add basic Backdrop compatibility (issue) (diff)

    If you look at the diff, you can see that instead of simply adding the backdrop = 1.x line to the .info file, I replaced the core = 7.x line with it (since the latter is Drupal-specific and does not need to be in the Backdrop version).

    With that change, the module works! Here it is in action on my Backdrop site:

    Cancel account using password

    (Also visible in this screenshot is a nice effect of Backdrop's layout system: Editing pages like this one, even though they are using the default front-end Bartik theme, have a more streamlined, focused layout than normal front-end pages of the site, without the masthead and other standard page elements.)

  2. Other code changes for this small module weren't strictly necessary, but I made them anyway to have a fully-compatible Backdrop codebase:
    • Replace usage of "drupal" with "backdrop" in the code (issue) (diff)
    • Use method on the user account object to determine its ID (issue) (diff)
  3. Next up, I want to get my module listed on the official list of Backdrop contributed projects (currently this list is on GitHub, although it may eventually move to the main Backdrop CMS website).

    I read through the instructions for applying to the Backdrop contributed project group. They're relatively simple, and I've already done almost everything I need above. The one thing I'm missing is that Backdrop requires a README.md file in the project root with some standard information in it (I like that they're enforcing this; it should help developers browsing the module list a lot), and it also requires a LICENSE.txt file. These were both easy to create following the provided templates and copying some information from the module's Drupal.org project page:

    Once that's done, and after reading through the rest of the instructions and making sure I agreed with them, I proceeded to create an issue:

    Application to join contrib team

    In my case it was reviewed and approved within a few hours (perhaps helped by the fact that I was porting a small module), and I was given access to the Backdrop contributed project team on GitHub.

  4. To get the module transferred from my personal GitHub account to the official Backdrop contrib list, I followed GitHub's instructions for transferring a repository.

    They are mostly straightforward. Just make sure to use "backdrop-contrib" as the name of the new owner (who you are transferring the repository to):

    Transfer repository to backdrop-contrib

    And make sure to check the box that gives push access to your repository to the "Authors" team within the Backdrop Contrib group (if you leave it as "Owners", you yourself wouldn't be able to push to it anymore):

    Grant access to the Authors team

    That's all it took, and my module now appears on the official list.

    You'll notice after you do this that all the URLs of your project have changed, although the old ones redirect to the new ones. That's why if you follow many of the links in this post, which point to URLs like https://github.com/DavidRothstein/user_cancel_password_confirm, you'll see that they actually redirect you to https://github.com/backdrop-contrib/user_cancel_password_confirm.

    For the same reason, you can keep your local checkout of the repository pointed to the old URL and it will still work just fine, although to avoid any confusion you might want to either do a fresh clone at this point, or run a command like the following to update the URL:

    git remote set-url origin [email protected]:backdrop-contrib/user_cancel_password_confirm.git
    

With the above steps, we’re all set; the module is on GitHub and can be developed further for Backdrop there.

But what happens later on when I make a change to the Drupal version of the module and want to make the same change to the Backdrop version (certainly a common occurrence)? Do I have to repeat the same changes manually in both places? Luckily the answer is no. In part 3 of this series, I’ll explain how to link the Backdrop module to the Drupal.org version and maintain them simultaneously. Stay tuned!

Further Backdrop Resources

Do you have any thoughts or questions, or experiences of your own trying to port a module to Backdrop? Leave them in the comments.

Feb 03 2015
Feb 03

Part 1 - Reuse the Same Code

In mid-January, the first version of Backdrop CMS was released. Backdrop is a fork of Drupal that adds some highly-anticipated features and API improvements to the core Drupal platform while focusing on performance, usability, and developer experience.

When an open-source fork makes the news, it's often because it was born from a fierce, acrimonious battle (example: Joomla forking from Mambo); the resulting projects compete with each other on the exact same turf and developers are forced to choose sides. Backdrop's goal, however, is not to destroy or replace the original Drupal project. Rather, it aims to be a "friendly fork" that focuses on Drupal's traditional audience of site builders and developers, an audience which the Backdrop founders believe are being slowly left behind by the Drupal project itself.

Because of this, I expect that many existing Drupal developers will not want to choose between the platforms, but instead will continue working with Drupal while also beginning to use Backdrop. In this series of blog posts, I will explain how a module (or theme) developer can take a Drupal project they currently maintain and support it for Backdrop as well, while keeping duplicate work to a minimum.

  • In part 1 (this post), I'll show how for some modules, the exact same code can be used with both Drupal and Backdrop.
  • In part 2, I'll explain what to do when you want to port a Drupal module to a separate Backdrop version and get it up and running on GitHub.
  • In part 3, I'll explain how to link the Backdrop module to the Drupal.org version and maintain them simultaneously.

Sometimes the Same Exact Code can be Used With Both Drupal and Backdrop

To start things off let's look at Field Reference Delete, a Drupal 7 module I maintain which does some behind-the-scenes cleanup in your database when entities such as nodes or taxonomy terms are deleted on the site. It's a moderately-complex module which makes heavy use of Drupal's field and entity systems.

To make this or any Drupal module work with Backdrop, there is one step that is always required: Adding a backdrop = 1.x line to the .info file to inform Backdrop core that the code is Backdrop-compatible.

Easy enough. The big question is what changes are required beyond that?

Checking for Changes

Although Backdrop is not 100% compatible with Drupal 7 (due to the features and API improvements it adds) it aims to be backwards-compatible as much as possible, for example by including a compatibility layer to allow Drupal functionality that is deprecated in Backdrop to still work correctly.

The Backdrop change records and module conversion guide provide technical advice for developers on how and when to upgrade their code. The biggest changes are probably the configuration management system (Backdrop’s replacement for Drupal 7’s variable API and other configuration that was previously stored in the database) and layout system (which removes much of the functionality of the Drupal 7 Block module in favor of a newer, more powerful Layout module).

If your Drupal 7 module makes heavy use of these systems, it’s likely you’ll want to make some changes in order to work with Backdrop. However, the compatibility layer means that you might not actually need to. For example, Backdrop retains Drupal 7’s variable API (although it is marked as deprecated and is not as powerful as the configuration system which replaces it). So your code might still work even if it uses this system. It really depends on the details of how your module works, so the best advice is to test it out and see what (if anything) is broken.

It’s also worth noting that because Backdrop was forked from an early version of Drupal 8 (not from Drupal 7) it inherited a smattering of changes that were added to Drupal 8 early in the release cycle. Not all of these have made it into the list of Backdrop change records yet, although work is ongoing and people are adding them as they are noticed.

In the case of Field Reference Delete, I tested the module on Backdrop and it worked fine. I also skimmed the change records and module conversion guide mentioned above and didn't see anything that obviously needed to change. Even though entities in Backdrop have been converted to classed objects and the field API has been converted to use the configuration management system, Field Reference Delete’s heavy use of the entity and field systems still didn’t require that any changes be made. All I had to do to get the module working was add that one backdrop = 1.x line to the .info file.

Adding the One Line of Code on Drupal.org

Interestingly enough, since Drupal will happily ignore a backdrop = 1.x line in a module's .info file, it's possible to simply add that code to the Drupal.org version of the module and use the same version of the module for either Drupal or Backdrop. I did that in this issue; the resulting diff is simply this:


diff --git a/field_reference_delete.info b/field_reference_delete.info
...
name = Field reference delete
description = Immediately removes references to a deleted entity from fields stored in an SQL database.
core = 7.x
+backdrop = 1.x

Drupal uses the core = 7.x line to determine Drupal compatibility, and Backdrop uses the backdrop = 1.x line to determine Backdrop compatibility. The 7.x-1.0-beta1 release of Field Reference Delete contains the above change and can be used equally well on a Drupal or Backdrop site. Simple!

There are some downsides to doing this, however:

  • Although no changes to the module code may be strictly required, there are usually optional (and non-backwards-compatible) changes that can be made to better integrate with new Backdrop features.
  • It is hard for Backdrop users and developers to find the module and know that it's compatible with Backdrop. I tried to improve its discoverability by adding a "Backdrop compatibility" tag to the above-mentioned issue, and I also put a note on the project page explaining that Backdrop is supported. These aren't ideal, but should help; perhaps a standard will eventually catch on but there isn't a formal one yet.

Despite these disadvantages, for the time being I'd like to just have one copy of the code for this particular module (hosted in one place), and it's nice to know that's possible.

In part 2 of this series, I’ll take a look at a different module I maintain where it isn’t possible to use the same exact code for Drupal and Backdrop, and I’ll show how I went through the official process for porting my module to Backdrop and getting it hosted on GitHub. Stay tuned!

Further Backdrop Resources

Do you have any thoughts or questions, or experiences of your own trying to port a module to Backdrop? Leave them in the comments.

Mar 19 2013
Mar 19

By default, a Drupal site allows undefined arguments on any system path instead of returning 404 Not Found (e.g. http://drupal.org/node/1/asdlfkjadlfkdjaflakjsldkfa).  I created the Strict 404 module to override this behavior and enforce 404 responses for paths that are not actually defined in the menu system.

 

Some contrib modules rely on the undefined argument behavior (e.g. for admin pages) so you may want to enable Strict 404 only for certain path patterns (usually vistor-facing pages).  The module provides an admin page for selectively enabling Strict 404 responses. 

Mar 11 2013
Mar 11

Drupal's content modeling toolbox makes it easy to add simple values to any content type -- numbers, text fields, dates, and so on -- but odd gaps pop up occasionally. Numeric ranges, for example, can be frustrating: While you could add separate "minimum" and "maximum" fields to a content type, it's a bit frustrating to split conceptually related set of values into distinct fields. Thankfully, the Range module can fill the gap.

Screenshot of administration screen

Range field gives site builders and administrators a single field type with two values: minimum and maximum. Variants for each basic numeric type are available (Float range, integer range, and so on), and the module provides enough formatting options to capture most of the basic use cases for the field. Views integration is also baked in, making it possible to sort and filter by the range values as well.

Screenshot of resulting change to site

A handful of existing feature requests for the module suggest potential improvements: the Views filter would be even more useful if it could pull up nodes nodes whose ranges included the incoming filter value, for example. While it's possible to simulate that using a pair of filters working together, it's definitely not as convenient. That request aside Range is a clean and simple field module that fills an awkward gap. If your content types need ranges, it's just what you need!

*/
Mar 04 2013
Mar 04

A freshly-installed copy of Drupal core includes the handy ability to generate short "teaser" text from a long node automatically. You can choose the character count that will be used, but unfortunately that's all: if you need to break on word boundaries or want to make it clear that the text is a truncated version of the full article, you're out of luck. If you need more flexibility, the Smart Trim module can help.

Screenshot of administration screen

Smart Trim adds a new set of formatting options to Drupal's Text, Long Text, and Long Text With Summary field types. Like the existing Summary formatter, it allows you to choose the truncation length -- but you can also truncate at a specific number of words, ensuring that the break won't fall awkwardly between words. An elipses can be inserted automatically at the end of the truncated text, and HTML can be stripped out to ensure that unclosed tags don't sneak into teasers. It's even possible to automatically add a "Read more…" link that points to the full view of the node itself. While that one's less useful on node teasers, it can be handy when you're using the truncated summary text in a highly streamlined view; it eliminates the need to add a separate field to contain a link to the node.

Screenshot of resulting change to site

Most of the options in Smart Trim are available in older modules for Drupal 5 and 6, or can be accomplished with a few snippets of custom code tucked in template files. This module collects all of them in one simple field formatter, however, and ensures the busy admins only need to grab one tool to make sure their teasers work just so.

*/
Jan 29 2013
Jan 29

Working with Lightbox2 and Colorbox

In this series, Lightboxes and Drupal 7, Kyle Hofmeyer walks you through working with two popular lightbox modules in Drupal 7: Ligthbox2 and Colorbox. The first video in the series is free, and explains what a lightbox is, and some things to consider when selecting which module to use.

Jan 29 2013
Jan 29

Working with Lightbox2 and Colorbox

In this series, Lightboxes and Drupal 7, Kyle Hofmeyer walks you through working with two popular lightbox modules in Drupal 7: Lightbox2 and Colorbox. The first video in the series is free, and explains what a lightbox is, and some things to consider when selecting which module to use.

Jan 23 2013
Jan 23

This week is the first part of a new mini-series on Lightboxes and Drupal 7. Lightboxes on the web will show larger or full content in an overlay on the current page, dimming the background, when a smaller version, or thumbnail, is clicked on. It's a great way to allow users to browse, and view full content for many different items, without having to leave the listing page they are on and keep clicking back and forth. In today's lessons, we have a FREE lesson which explains what a lightbox is, and gives an overview of some things to consider when choosing a lightbox Drupal module. Then we cover getting set up with the two most popular lightbox module choices: Lightbox2 module and Colorbox module.

Next week we'll wrap up with more lessons about lightboxes, actually putting the modules to use, and looking at the differences when working with images versus regular content and views.

Jan 18 2013
Jan 18

In honor of Internet Freedom Day I'm Bumping This Post || RIP Aaron Swartz

Help spread the word by joining this Thunderclap: https://www.thunderclap.it/projects/1039-internet-freedom-day

What Is SOPA? Get me spyglass, I'll warrant ye!

(original text Pirated from Gizmodo.com)

If you hadn't heard of SOPA before, you probably have by now: Some of the internet's most influential sites—Reddit and Wikipedia among them—are going dark to protest the much-maligned anti-piracy bill. But other than being a very bad thing, what is SOPA? And what will it mean for you if it passes?
SOPA is an anti-piracy bill working its way through Congress...

House Judiciary Committee Chair and Texas Republican Lamar Smith, along with 12 co-sponsors, introduced the Stop Online Piracy Act on October 26th of last year. Debate on H.R. 3261, as it's formally known, has consisted of one hearing on November 16th and a "mark-up period" on December 15th, which was designed to make the bill more agreeable to both parties. Its counterpart in the Senate is the Protect IP Act (S. 968). Also known by its cuter-but-still-deadly name: PIPA. There will likely be a vote on PIPA next Wednesday; SOPA discussions had been placed on hold but will resume in February of this year.
...that would grant content creators extraordinary power over the internet...

The beating heart of SOPA is the ability of intellectual property owners (read: movie studios and record labels) to effectively pull the plug on foreign sites against whom they have a copyright claim. If Warner Bros., for example, says that a site in Italy is torrenting a copy of The Dark Knight, the studio could demand that Google remove that site from its search results, that PayPal no longer accept payments to or from that site, that ad services pull all ads and finances from it, and—most dangerously—that the site's ISP prevent people from even going there.
...which would go almost comedically unchecked...

Perhaps the most galling thing about SOPA in its original construction is that it let IP owners take these actions without a single court appearance or judicial sign-off. All it required was a single letter claiming a "good faith belief" that the target site has infringed on its content. Once Google or PayPal or whoever received the quarantine notice, they would have five days to either abide or to challenge the claim in court. Rights holders still have the power to request that kind of blockade, but in the most recent version of the bill the five day window has softened, and companies now would need the court's permission.

Sep 24 2012
Sep 24

Well, the itch to blog rapidly with quick screenshots finally got to me, and I spent the last couple of days re-working the Drupal Evernote module to get it functional with the new Evernote API updates and Oauth.

One failing of the first module was that there were a lot of steps involved to get it set up. In this new version, I've simplified the options quite a bit. Today I started using it in practice on chrisshattuck.com, and it seems to be working pretty smoothly. Before I release it into the wild, though, I'm going to give it a bit to work out any kinks. Man, it's a lot of fun being able to blog straight from Evernote. I've also set up a system to post directly to Facebook and Twitter as well with some simple tagging (I'll talk more about that later).

I went ahead and added it up on Github so folks can goof around with it until I can get a chance to re-grok the Drupal module tagging scheme. Also, right now it's just for Drupal 6 (since that's where my itch is).

Here's a screenshot of the admin page:

Evernote Settings Content type: him ., ? 10 ChriS Shant.l(k OCR tut Do sync syncs up only the 100 ?atod notes. will the in.

Aug 22 2012
Aug 22

Posted Aug 22, 2012 // 0 comments

When Moshe Weitzman posted his idea for a Drupal community initiative called DrupalGive on Drupal.org, we knew we wanted to get on board. As Moshe simply explains, Drupalgive is a page that organizations publish on their website to highlight the ways they have contributed to Drupal with the intent to educate clients and partners about the Drupal community and also "nudge" other organizations to contribute.

By nature, open source software is dependent on contribution. As Drupal matures, organizations are using it to build bigger, more complex websites. It is therefore more important than ever to contribute and share within our community in order encourage further innovation.

We are proud to announce the launch of our own DrupalGive page, designed by Dave Ruse and developed by Tirdad Chaharlengi and Josh Cooper.

Our page highlights 4 different ways we contribute to Drupal:

Modules

We recently posted a blog about our contributions to the Large Scale Drupal Initiative (LSD)  Specifically Site Preview System, read more about the project and this module here.

Events

We are a proud silver sponsor of DrupalCon Munich. We've been busy in Munich with some great collaborative sessions:

Distributions

We maintain 4 distributions: OpenPublic, OpenPublish, Open Atrium and Managing News.  We are excited about the most recent OpenPublish release including a new demo theme: Gazette.  Stay tuned for the impending OpenPublic release!

Presentations

We make sure we post as many of our session slides as we can, to promote Drupal Learning. Look out for our DrupalCon Munich session slides posted soon to our slideshare.

We had a lot of fun putting our Drupalgive page together, we look forward to our contribution lists growing, and using other organization's Drupalgive pages to stay informed and up to date on the latest Drupal contributions. 

As marketing coordinator at Phase2, Annie is involved in the open source events in the New York metro area. She really enjoys engaging the lovely Drupal Community and promoting Phase2 to prospects, clients, staff members, and the greater ...

Aug 17 2012
Aug 17

Posted Aug 17, 2012 // 4 comments

The Site Preview System Drupal module is a framework for previewing a site with a set of conditions active. Here is a video to introduce you to what the module does. SPS was developed out of the LSD CSI project as part of a suite of modules.

The Site Preview System works by accepting conditions, generating a list of override revisions and altering the page to display the correct revision on the page.

Modules that came out of the CSI project

Modules that are currently integrated with SPS.

Workflow

Layout

Entities

  • Nodes
  • Any other entity that supports Revisions

If you are going to be in Drupalcon Munich please come to one of the sessions or BOF

Team Architect at Phase2, Neil thrives on working with the development team to come up with creative implementations for complex situations. Neil enjoys pushing Drupal past its boundaries to implement cutting edge concepts, and has a ...

Mar 14 2012
Mar 14

When we last discussed State Machine, we highlighted how easy it was for developers to create custom workflows via the State Machine API.

The goal of State Machine is to provide an API first approach to workflows within Drupal. A simple user interface is included, but the developer ultimately has power and flexibility to extend and customize their workflows across various environments as they see fit.

We also reviewed State Flow, a sub-module packaged with State Machine which provides you with a base workflow, and showed how Energy.gov extends State Flow for its own custom needs.

We’re happy to share that State Machine has released a 2.x branch sporting some great new features to make your workflows even better.

Easier to Alter

The method for implementing a custom workflow changed from using variable_set() for defining the node types that should implement your custom workflow to using hook_state_flow_machine_type_alter(). Using Energy.gov as an example, we simply assign the machine type to match the key we declared in hook_state_flow_plugins().

/**
 * Implements hook_state_flow_plugins().
 */

function

energy_workflow_state_flow_plugins

() {
$info = array();
$path =

drupal_get_path

(‘module’, ‘energy_workflow’) . ‘/plugins’;
$info[‘energy_workflow’] = array(
‘handler’ => array(
‘parent’ => ‘state_flow’,
‘class’ => ‘EnergyWorkflow’,
‘file’ => ‘energy_workflow.inc’,
‘path’ => $path,
),
);
return $info;
} /**
 * Implements hook_state_flow_machine_type_alter()
 *
 * @param string $machine_type
 * @param object $node
 */

function

energy_workflow_state_flow_machine_type_alter

(&$machine_type, $node) {
$machine_type = ‘energy_workflow’;
}

You can also easily have multiple workflows by adding logic to change the machine type based on node type, instead of having to declare them as variables.

Bulk Revision Editing

A new administration page puts more power in users hands by allowing publishers to use your defined states and events on many items at once.

Similar to the philosophy of Views Bulk Operations, this is a neat feature for users who may want to fire events across multiple revisions.

For example, if you have micro-sites or a group of pages that all need to be launched simultaneously, State Flow includes a set of batch operations to publish those node revisions at once.

Revisions are administered by filters and operations. The filters dictate which revisions are selectable by administrators when they are applied. The operations serve as actions to perform on the revisions once they are selected.

For developers, these filters and operations are completely alterable and are packaged with their own hooks! This provides an opportunity to create unique bulk administration pages for your users.

Want to create a custom filter? Simply invoke hook_node_revision_filters() and add your filter to the form array. You can then invoke hook_query_node_revision_alter() to enact whatever logic you want for that custom filter.

Use cases include filters for revisions by taxonomy, the existence of certain field value, or even user access.

Here is an example of a tags filter and its query callback:

/**
 * Implements hook_node_revision_filters()
 */

function

mymodule_node_revision_filters

() {
$filters = array();

  //function to get tags
  $options = mymodule_tags_callback();

  $filters[‘mymodule_tags’] = array(
    ‘form’ => array(
      ‘#type’ => ‘select’,
      ‘#title’ => t(‘Content Tag’),
      ‘#options’ => $options,
    ),
  );

  return $filters;
}

/**
 * Implements hook_query_node_revision_alter()
 */

function

mymodule_query_node_revision_alter

(

QueryAlterableInterface

$query) {
// Get the filter form the session
if ($filters = $query->getMetaData(‘filters’)) {
if ($filter = isset($filters[‘elc_workflow_tags’])

?

$filters[‘mymodule_tags’] : NULL) {
/**
        * Custom code to join other tables
        */

}
}
}

This also true for custom operations. Utilizing hook_node_revision_operations(), we can easily create custom actions, in bulk, with the revisions exposed in this administration page.

Scheduling

Last, but not least, State Machine 2.x introduces workflow scheduling to allow users to set a day and time via the Date Popup module of when a node should transition to a defined state. This is done via a new submodule called State Flow Schedule.

State Flow Schedule extends State Flow to provide you with a default Scheduled state and Schedule event. Once a date and time is defined, the value is passed as a log message so users can easily view the workflow log of a node and check when it will transition.

Scheduling is heavily tied to cron. This means content won’t actually transition to the desired state until cron runs, so you should setup your environment accordingly.

Scheduling can also be extended. In Energy.gov, we had an additional event called Immediately Schedule to allow those with the appropriate permissions to skip the workflow for content starting in the draft state.

To utilize this, simply define the new event, then add it to array of definable scheduling events via hook_state_flow_schedule_events_alter().

/**
 * @file
 * Energy.gov implementation of State Flow, an extension of the State Machine class
 */

class EnergyWorkflow extends StateFlow {

  public function init() {
    // Initialize states
    $this->create_state(‘draft’, array(
      ‘label’ => t(‘Draft’),
    ));
    $this->create_state(‘needs review’, array(
      ‘label’ => t(‘Needs Review’),
    ));
    $this->create_state(‘approved’, array(
      ‘label’ => t(‘Approved’),
    ));
    $this->create_state(‘unpublished’, array(
      ‘label’ => t(‘Unpublished’),
    ));
    $this->create_state(‘published’, array(
      ‘label’ => t(‘Published’),
      ‘on_enter’ => array($this, ‘on_enter_published’),
      ‘on_exit’ => array($this, ‘on_exit_published’),
    ));
    $this->create_state(‘scheduled’, array(
      ‘label’ => t(‘Scheduled’),
      ‘on_exit’ => array($this, ‘on_exit_scheduled’),
    ));

    // Initialize events
    $this->create_event(‘for review’, array(
      ‘label’ => t(‘For Review’),
      ‘origin’ => ‘draft’,
      ‘target’ => ‘needs review’,
    ));
    $this->create_event(‘immediate publish’, array(
      ‘label’ => t(‘Immediate Publish’),
      ‘origin’ => ‘draft’,
      ‘target’ => ‘published’,
      ‘guard’ => ‘energy_workflow_guard_publisher’,
    ));
    $this->create_event(‘approve’, array(
      ‘label’ => t(‘Approve’),
      ‘origin’ => ‘needs review’,
      ‘target’ => ‘approved’,
      ‘guard’ => ‘energy_workflow_guard_editor’,
    ));
    $this->create_event(‘reject’, array(
      ‘label’ => t(‘Reject’),
      ‘origin’ => ‘needs review’,
      ‘target’ => ‘draft’,
      ‘guard’ => ‘energy_workflow_guard_editor’,
    ));
    $this->create_event(‘publish’, array(
      ‘label’ => t(‘Publish’),
      ‘origin’ => array(‘approved’, ‘scheduled’),
      ‘target’ => ‘published’,
      ‘guard’ => ‘energy_workflow_guard_publisher’,
    ));
    $this->create_event(‘unpublish’, array(
      ‘label’ => t(‘Unpublish’),
      ‘origin’ => ‘published’,
      ‘target’ => ‘unpublished’,
      ‘guard’ => ‘energy_workflow_guard_publisher’,
    ));
    $this->create_event(‘to draft’, array(
      ‘label’ => t(‘To Draft’),
      ‘origin’ => array(‘needs review’, ‘approved’, ‘unpublished’, ‘scheduled’),
      ‘target’ => ‘draft’,
    ));
    $this->create_event(‘schedule’, array(
      ‘label’ => t(‘Schedule’),
      ‘origin’ => ‘approved’,
      ‘target’ => ‘scheduled’,
      ‘guard’ => ‘state_flow_guard_schedule’,
    ));
    $this->create_event(‘immediate schedule’, array(
      ‘label’ => t(‘Immediate Schedule’),
      ‘origin’ => ‘draft’,
      ‘target’ => ‘scheduled’,
      ‘guard’ => array(‘energy_workflow_guard_publisher’, ‘state_flow_guard_schedule’),
    ));
  }

  /**
   * Other class info
   */

}

/**
 * Implements hook_state_flow_schedule_events_alter()
 *
 * @param array $scheduled_events
 */

function

energy_workflow_state_flow_schedule_events_alter

(&$scheduled_events) {
$scheduled_events[] = ‘immediate schedule’;
}

State Machine 3.x

These are great new features, and State Machine 3.x is promising to be even better!

The plan is to refactor State Flow to be entity agnostic and ultimately connect with Workbench Moderation 2.x.

Come out to the 2012 Drupalcon Denver workflow-based module collaboration sprint with Steve Persch and myself to get on the ground floor of this very unique and important collaboration.

Jan 29 2012
Jan 29

Search APIAffinity Bridge's Katherine Bailey and Tom Nightingale gave an awesome introduction to the Search API module at the recent Vancouver Drupal User Group's meeting.

Affinity Bridge was using the Drupal 6 Searchlight module previously. Searchlight had three things going for it:

  1. You could plug it into the popular Apache Solr search platform.
  2. you could plug it into the Sphinx search platform.
  3. It worked with the Views module for displaying search tools and search results.

What it didn't have was compatibility with Drupal 7. (There's a broken fork of it on Github, but that's it.) Katherine heard about Search API and thought it might work. This comment by Robert Douglass largely convinced her:

“I can tell by the usage statistics that you haven't tried the Search API module yet! Run, don't walk, to the download page and give it a go. You'll have to pick yourself up a copy of the Entity module as well, but then you're all set.” - http://groups.drupal.org/node/95974

Search API is all about adaptability, Katherine says.

Setting Up a Search

Search API StructureWhen setting up a search, there are three categories to think about:

  1. How will the search perform?
  2. What will the results look like?
  3. What extra features will be available on the search page? (ie. facets, “more like this”, spelling suggestions, etc.)

Search API lets you answer these questions no matter the search platform you are using.

On Search API's main administration page, you accomplish:

  1. choosing your search server/platform - give it a name and description, and set the path to connect to Solr, or whatever backend you are using.
  2. setting up an index - again, give it a name; then specify what entity type it will index. “Node” - Drupal's term for what is typically referred to as a “page” on a web site, is the most common entity specified.

You can perform searches across indexes.

Your next steps will be:

  1. choosing the fields of your content that you want indexed. Content management systems generally parse out elements of a page, or node, into fields. “Title” and “Body” are typical content fields – and ones you might typically index. Other field examples include date that content is posted or updated, author and comments.
  2. If you want to search on particular fields, use a string.
  3. If you cannot find the field you want indexed, check the “related fields” section.
  4. The Workflow tab allows you to customize how your index works. “Aggregated fields,” for example, let you create a new field based on processors.

Search API, Katherine says, is “beautifully architected” and thus easy to extend with your own custom plugins.

Facet API

Facets API is a module that works along side Search API. Facets of a search are like categories. They allow users to search by author, organization, topic, (subtopic ad infinitum!), date ranges, etc. Facets API lets you create guided searches for your users, and display them in user friendly ways. You can create dropdown lists, for example, that users click or un-click to narrow and/or widen search results. Facets API works with search modules other than Search API – you can use it with whatever search module you decide to you. You can only facet what is indexed.

Case Study

Tom Nightingale walked through Affinity Bridge's search set up for the University of Ottawa. This set up included a calendar system to manage events for faculties, sub organizations within faculties, and their associated staff, students and potential students. Categories, in effect, shifted from faculty and from sub-organization to sub-organization, all having differing needs. Additionally, as a bilingual university, content existed in both french and english.

Hurdles

  1. After defining servers and indexes, Affinity Bridge thought “great, we can use the Views module, date, slideshow, etc.”; however, Views for Drupal 7 is very modular and made to work with the MySql database while their implentation of the Search API swapped out the Sql backend for Solr. Workarounds were needed.

  2. Creating lists of events, grouped by organizations, also needed workarounds because the concept of aggregation doesn't exist in Solr.

  3. Drupal's Date module supports repeating dates, and stores this data as a multi-value field; however, Solr has its own native date handling which does not line up with the Sql-based repeating date data storage. Affinity Bridge used the concepts of entities from Drupal 7 to deal with this issue: by creating an entity, they could split the repeating date values into separate entities.

  4. Internationalization: one of required fields on entity is language. Search API indexes language so you can use Views filters to display the current language. However, if you want to do more complicated language filtering: showing all french entries if they exist, or english entries if no french entry exists, for example, Search API cannot handle it. Again, workarounds needed to be implemented.

Search API Summary

  • Anything that's an entity can automatically be indexed.
  • It is possible to index non-entity content by specifying an external source of data and sending it to a Solr index.
  • You can set up multiple indexes, and even perform multi-index searches, using the Search API multi-indexes module.
  • You can use multiple search servers, including Mongo DB.
  • There is a powerful system of plugins available for altering data during the indexing process, and tons of hooks and alter hooks.
  • Benefits:
  • all configurations are exportable to features,
  • excellent views integration, and
  • faceting, via the Facet API module.

Resources

Jan 28 2012
Jan 28

Search APIAffinity Bridge's Katherine Bailey and Tom Nightingale gave an awesome introduction to the Search API module at the recent Vancouver Drupal User Group's meeting.

Affinity Bridge was using the Drupal 6 Searchlight module previously. Searchlight had three things going for it:

  1. You could plug it into the popular Apache Solr search platform.
  2. you could plug it into the Sphinx search platform.
  3. It worked with the Views module for displaying search tools and search results.

What it didn't have was compatibility with Drupal 7. (There's a broken fork of it on Github, but that's it.) Katherine heard about Search API and thought it might work. This comment by Robert Douglass largely convinced her:

“I can tell by the usage statistics that you haven't tried the Search API module yet! Run, don't walk, to the download page and give it a go. You'll have to pick yourself up a copy of the Entity module as well, but then you're all set.” - http://groups.drupal.org/node/95974

Search API is all about adaptability, Katherine says.

Setting Up a Search

Search API StructureWhen setting up a search, there are three categories to think about:

  1. How will the search perform?
  2. What will the results look like?
  3. What extra features will be available on the search page? (ie. facets, “more like this”, spelling suggestions, etc.)

Search API lets you answer these questions no matter the search platform you are using.

On Search API's main administration page, you accomplish:

  1. choosing your search server/platform - give it a name and description, and set the path to connect to Solr, or whatever backend you are using.
  2. setting up an index - again, give it a name; then specify what entity type it will index. “Node” - Drupal's term for what is typically referred to as a “page” on a web site, is the most common entity specified.

You can perform searches across indexes.

Your next steps will be:

  1. choosing the fields of your content that you want indexed. Content management systems generally parse out elements of a page, or node, into fields. “Title” and “Body” are typical content fields – and ones you might typically index. Other field examples include date that content is posted or updated, author and comments.
  2. If you want to search on particular fields, use a string.
  3. If you cannot find the field you want indexed, check the “related fields” section.
  4. The Workflow tab allows you to customize how your index works. “Aggregated fields,” for example, let you create a new field based on processors.

Search API, Katherine says, is “beautifully architected” and thus easy to extend with your own custom plugins.

Facet API

Facets API is a module that works along side Search API. Facets of a search are like categories. They allow users to search by author, organization, topic, (subtopic ad infinitum!), date ranges, etc. Facets API lets you create guided searches for your users, and display them in user friendly ways. You can create dropdown lists, for example, that users click or un-click to narrow and/or widen search results. Facets API works with search modules other than Search API – you can use it with whatever search module you decide to you. You can only facet what is indexed.

Case Study

Tom Nightingale walked through Affinity Bridge's search set up for the University of Ottawa. This set up included a calendar system to manage events for faculties, sub organizations within faculties, and their associated staff, students and potential students. Categories, in effect, shifted from faculty and from sub-organization to sub-organization, all having differing needs. Additionally, as a bilingual university, content existed in both french and english.

Hurdles

  1. After defining servers and indexes, Affinity Bridge thought “great, we can use the Views module, date, slideshow, etc.”; however, Views for Drupal 7 is very modular and made to work with the MySql database while their implentation of the Search API swapped out the Sql backend for Solr. Workarounds were needed.

  2. Creating lists of events, grouped by organizations, also needed workarounds because the concept of aggregation doesn't exist in Solr.

  3. Drupal's Date module supports repeating dates, and stores this data as a multi-value field; however, Solr has its own native date handling which does not line up with the Sql-based repeating date data storage. Affinity Bridge used the concepts of entities from Drupal 7 to deal with this issue: by creating an entity, they could split the repeating date values into separate entities.

  4. Internationalization: one of required fields on entity is language. Search API indexes language so you can use Views filters to display the current language. However, if you want to do more complicated language filtering: showing all french entries if they exist, or english entries if no french entry exists, for example, Search API cannot handle it. Again, workarounds needed to be implemented.

Search API Summary

  • Anything that's an entity can automatically be indexed.
  • It is possible to index non-entity content by specifying an external source of data and sending it to a Solr index.
  • You can set up multiple indexes, and even perform multi-index searches, using the Search API multi-indexes module.
  • You can use multiple search servers, including Mongo DB.
  • There is a powerful system of plugins available for altering data during the indexing process, and tons of hooks and alter hooks.
  • Benefits:
  • all configurations are exportable to features,
  • excellent views integration, and
  • faceting, via the Facet API module.

Resources

Jan 20 2012
Jan 20

Drupal often makes certain things so easy to do, that we as developers don’t take the time to consider the alternatives. Lately I’ve been giving a lot of thought to the module dependency system and possible alternatives. The main reason for this is that dependencies create a more brittle overall system which in turn makes the code more difficult to maintain and re-use.

Scenario 1

I’ve seen many install profiles and modules that include modules like overlay or toolbar as a dependency. In almost every case, this was done so that those modules would be installed by default. The problem with using the dependency system is that these modules cannot be removed without modifying the code. If you wanted to use admin_menu in place of toolbar, you would need to first remove the dependency from the info file.

Solution

This one has a simple solution. Rather than using dependencies to install modules, use a simple install hook with module_enable.

/**
 * Implements hook_install().
 */

function example_install() {
  // Enable optional modules.
  module_enable(‘toolbar’, ‘overlay’);
}

Scenario 2

Say we have a module that creates a basic article node, with a title, body and taxonomy field for tags. Typically we would make taxonomy module a dependency of the article module. In the spirit of reusable code, we want to make our article module as self-contained as possible, so that we can use it on all of our Drupal projects.

But what if we have a project that has no need for taxonomy? Must we always enable the taxonomy module to use our article module, even if there is no need?

Solution

The answer to our problem comes in the form of a couple of new hooks for Drupal 7: hook_modules_enabled and hook_modules_disabled, give us the perfect opportunity to act upon new dependencies being enabled and disabled.

First we will create a helper function for our optional functionality. In this case, we want to create a tags vocabulary and add a taxonomy reference field to the article node type, only if the taxonomy module is available.

/**
 * Add the "tags" taxonomy field to a node type.
 */

function article_add_tags_field($type) {
  if (module_exists(‘taxonomy’)) {
    // create the tags vocabulary and add the field to the bundle, making sure
    // to check if they already exist.
  }
}

Next, we implement hook_enable to create our article node type. This technique works equally well with hook_install so choose the hook that is most appropriate for your situation. Notice that we are checking the existence of the taxonomy module before calling our helper function. This check could be skipped since it also happens in the helper, but I think checking it here reinforces the idea that this is an optional feature.

/**
 * Implements hook_enable().
 */

function article_enable() {
  // Borrowed from standard.install.
  $type = node_type_set_defaults#40;array(
    ‘type’ => ‘article’,
    ‘name’ => st(‘Article’),
    ‘base’ => ‘node_content’,
    ‘description’ => st(‘Use <em>articles</em> for time-sensitive content…’),
    ‘custom’ => 1,
    ‘modified’ => 1,
    ‘locked’ => 0,
  ));
  node_type_save($type);
  node_add_body_field($type);
  if (module_exists(‘taxonomy’)) {
    article_add_tags_field($type);
  }
}

Lastly, we implement hook_modules_enabled. This hook is called by the module system any time one or more modules are enabled. Here is where we have the opportunity to look for newly enabled modules that our module knows how to integrate with. If the taxonomy module is in the list of modules that have been enabled, we can fire our helper function.

/**
 * Implements hook_modules_enabled().
 */

function article_modules_enabled($modules) {
  if (in_array(‘taxonomy’, $modules)) {
    article_add_tags_field(node_type_load(‘article’));
  }
}

For completeness, you should probably also implement hook_disable and hook_modules_disabled, but I’ll leave that as an exercise for you.

We are using these two methods to keep our dependency chain light and add in optional functionality on our Drupal 7 projects. I think there is an opportunity to build true support for these types of optional integrations into Drupal 8. I’d love to hear how others are handling this situation. How are you handling dependencies and integrations in your modules, especially ones meant to be used in a variety of contexts and situations?

Jan 09 2012
Jan 09

January 9th, 2012

In the Drupal community the phrase “There’s a module for that” is pretty much synonymous with Apple’s “There’s an app for that”. So it was a bit surprising when Trent, ImageX’s ‘walking module encyclopedia’, mentioned the lack of an existing module allowing developers to add links to the contextual links widgets in Drupal 7.

There are a number of Drupal 6 modules that create a nice shortcut to edit/delete a node or to configure a block without actually clicking through the entire menu structure. Developers that used any of these modules immediately added them to their own personal ‘must install’ module lists and as a result, and much to the delight of the community, this became a core feature in Drupal 7.

On it’s own the core module adds contextual link widgets to nodes and blocks and there are a number of modules that add their own links as well. Previously, the only way to add custom links was to create another smaller module (a step that a large part of the community wanted to be able to skip). So this is where the idea for a Custom Contextual Links (CCL) module came in… to give administrators an interface to add custom links that can be attached to nodes and blocks.

Version 1.0

The first version of the module had a relatively limited set of features. With it developers were able to create links that attached to either:

  • All nodes
  • All nodes of a specific content type
  • One specific node
  • All blocks
  • A specific block

The ability to use a node id token in link path was also added as a bonus. This works in connection with links attached to nodes and allows one to create dynamic links in connection with node objects.

Version 1.1

One of the first responses after the release of the stable version was in regards to the custom token. Lullabot’s Jeff Eaton who had previously written an article in Module Mondays pointed out that it would make more sense to use core tokens. So this was first in a list of changes to implement to help improve the module.

Custom Contextual Links Step 1. Use tokens in the URL and title.

Scott Jordison then recommended attaching links to views (This, by the way, is a shining example of why the Drupal community is so awesome. Instead of just requesting this feature Scott supplied a constant stream of patches that added this feature to CCL.) Now you can attach your custom link to:

  • All views
  • All displays of a specific view
  • A specific display of a view

These suggestions prompted a rewrite of the core of CCL to make it more modular. This involved removing the ability to add links to blocks and putting them into their own sub-module. The same applied to the views option.

The last thing to add was the ability to quickly publish/promote/sticky a node through the contextual widget. This feature might need extra attention in the future but for now you can attach a link to:

  • Publish/Unpublish
  • Promote/remove to/from front page
  • Make sticky/unsitcky
  • To nodes (all, by content type or specific). Users will only be able to see these links on nodes where they have the ‘update’ permission.

Custom Contextual Links Step 3. Attach custom links to all orspecific nodes/bloc

The final result looks like this:

Custom Contextual Links Module Final Result

Version 1.?

So what else is there to add? One thing on the agenda is to make all the settings exportable through features (time didn’t allow for this in Version 1.1). Revisiting access settings and looking into integration with rules is also an option to explore.

Have suggestions of your own? Try out the latest version of CCL and give your feedback.

Jan 05 2012
Jan 05

Hey Drupal developers and site administrators! Ever spend your time handling things like block placement and microsite deployments at the request of your content editors?

Hey content editors and managers! Ever wish you could just do this stuff without relying on your developers and administrators–or becoming a de facto site administrator yourself?

This one’s for you.

Three of our modules from 2011–co-sponsored by Energy.gov–empower content editors with greater independence and more effective execution tools.

BEAN

BEAN gives editors an easy way to plug blocks into any page on a website without the typical administrative wrangling.

On the technical side, the module creates blocks as entities. Fields are used to define block types (similar to node types). Just like nodes, instances of blocks can be created and placed into pages. More geekery in this Neil Hastings post on the BEAN module.

State Machine

State Machine provides content editors with additional options for non-disruptive content revisions via the State Flow base implementation. By creating a draft version of existing content, ongoing revisions can occur without interfering with site performance. When a revision is ready to publish, the published version (when applicable) is automatically archived and replaced with the latest approved copy.

Developers get a treat as well–the State Machine module is really an API. Through this API, State Machine extendable and exportable through plugins. Code-based workflows can be created for multiple sites and easily tested–without affecting existing content and functionality. Fredric Mitchell breaks it down (with extensive code samples) here.

OG Tasks

OG Tasks is an add-on for the Organics Group Module. OG Tasks automates many of the tasks associated with creating new groups–such as when launching a microsite. Instead of relying on developers or site administrators, content editors can manage pre-configured group creation with just a few mouse clicks.

Neat, right? Kudos to Neil Hastings, Fredric Mitchell, Roger Lopez, Joe Turgeon, and Tim Cosgrove for their work on bringing these modules to fruition!
Sep 21 2011
Sep 21

Update: Please see the code at https://gist.github.com/1460818 for a working example with the current bean code base.

In my previous post I talked about how we decided to leverage EntityFieldQUery as an alternative to Views for aggregating lists of content. In this post we'll be looking at how Treehouse created the Bean module to create and place blocks containing these lists of content.

What Are Beans?

The purpose of the Bean module is to create blocks as entities. We can build out block types, using fields as with any Drupal 7 entity, and have instances of blocks. The concept is similar to how node types are commonly used in Drupal 7. An individual instance of the block type can be placed on any number of pages.

Depending on the construction of the block type, different instances of the block type could have different display settings as well as content. As with nodes, we're not limited to simply adding content to a Bean block. We can use fields to configure the display parameters of the block. This can allow us to give an editor the ability to easily and quickly construct custom queries for blocks.

Let's look at a real example from Energy.gov where we used Beans to aggregate and display content.

Building A Content Listing

This example is from the Energy.gov News Landing Page. The requirement for this block was to collect recent article nodes, optionally filtered by some taxonomy terms, and allow for 3 different type of displays. Users should be able to construct a block, choosing which article types they want to display, which topics they want to restrict their search to, and how many nodes to display for each display type.

To accomplish this task, we created a article listing block type. Let's walk through some of the code used to construct the block type.

Building The Plugin

The Bean module uses the Ctools plugin architecture so defining one should look familiar to some Drupal developers. First, in your module file, tell the Bean module that you are defining new block types.

/**  
  * Implements hook_bean_types_api_info().  
  */

function

mymodule_bean_types_api_info

() {
return array( 'api' => 1);
}

/**
 * Implements hook_bean_types().
 */

function

mymodule_bean_types

() {
$plugins = array();
$plugin_path =

drupal_get_path

('module', 'mymodule') .
'/plugins/bean';

  $plugins['listing'] = array(
    'label' => t('Listing'),
    'handler' => array(
      'class' => 'listing_bean',
      'parent' => 'bean',
    ),
    'path' => $plugin_path,
    'file' => 'listing.inc',
  );

return $plugins;
}

We defined a "Listing" Block type. The block type will use the class "listing_bean" and extends the plugin "bean." The code for the listing block type can be found in /plugins/bean/listing.inc. Nice and simple. Since we are adding a new file, be sure to add the following line to your mymodule.info file:

"files[] = plugins/bean/listing.inc"

This tells the Drupal code registry to look in this file for code to autoload.

For more detail about the full API for block types, see the bean_type_plugin_interface in includes/bean.core.inc in the Bean module. The bean_plugin abstract class takes care of most of the work, but you can always use your own base class if you wish. If you extend the default bean plugin then there are three methods you need to implement:

  • Values: These are the properties that your plugin will be using along with their defaults.
  • Form: This is the form that is used when creating/editing the block.
  • View: This is the code used to render the block.

First we create the shell for the class.

/**
 * @file
 * Listing Plugin
 */
class

listing_bean

extends

bean_plugin

{
}

Here are some more detailed requirements for this block type.

  • Ability to define the number of records in each of the three display types.
  • Filter by article type, topic and intended audience.
  • Integration with OG
  • More link with custom URL and text.

The content we are displaying is of the "article" content type. This content type has an "article_type" vocabulary that defines if it's a blog article, news article, etc. Topics and audience are a vocabularies used to categorize articles. This gives us three different option filters for vocabularies.

Setting Up Properties and Values

Let's define what properties our block type will have.:

class listing_bean extends bean_plugin {
  public function values() {
    return array(
      'filters' => array(
        'term' => FALSE,
        'topic' => FALSE,
        'audience' => FALSE,
      ),
      'items_per_page' => array(
        'large_image' => 0,
        'small_image' => 0,
        'listing' => 0,
      ),
      'more_link' => array(
        'text' => '',
        'path' => '',
      ),
    );
  }
}

Building The Form

Each of the properties are set via the block creation form. Let's define the form.

class

listing_bean

extends

bean_plugin

{
public function

form

($bean) {
$form = array();

    $options = array();
    if ($vocabulary =
      taxonomy_vocabulary_machine_name_load('article_type')) {
      if ($terms = taxonomy_get_tree($vocabulary->vid)) {
        foreach ($terms as $term) {
          $options[$term->tid] =
            str_repeat('-', $term->depth) . $term->name;x
        }
      }
    }

    $topic_options = array();
    if ($vocabulary =
      taxonomy_vocabulary_machine_name_load('topics')) {
      if ($terms = taxonomy_get_tree($vocabulary->vid)) {
        foreach ($terms as $term) {
          $topic_options[$term->tid] =
            str_repeat('-', $term->depth) . $term->name;
        }
      }
    }

    $audience_options = array();
    if ($vocabulary =
      taxonomy_vocabulary_machine_name_load('audience')) {
      if ($terms = taxonomy_get_tree($vocabulary->vid)) {
        foreach ($terms as $term) {
          $audience_options[$term->tid] =
            str_repeat('-', $term->depth) . $term->name;
        }
      }
    }

    $form['filters'] = array(
      '#type' => 'fieldset',
      '#tree' => 1,
      '#title' => t('Filters'),
    );
    $form['filters']['term'] = array(
      '#type' => 'select',
      '#title' => t('Article Type'),
      '#options' => $options,
      '#default_value' => $bean->filters['term'],
      '#multiple' => TRUE,
      '#size' => 5,
    );
    $form['filters']['topic'] = array(
      '#type' => 'select',
      '#title' => t('Topic'),
      '#options' => $topic_options,
      '#default_value' => $bean->filters['topic'],
      '#multiple' => TRUE,
      '#size' => 10,
    );
    $form['filters']['audience'] = array(
      '#type' => 'select',
      '#title' => t('Audience'),
      '#options' => $audience_options,
      '#default_value' => $bean->filters['audience']
      '#multiple' => TRUE,
      '#size' => 10,
    );

    $form['items_per_page'] = array(
      '#type' => 'fieldset',
      '#tree' => 1,
      '#title' => t('Items per page'),
    );
    $form['items_per_page']['large_image'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of items with a large image'),
      '#description' => t('These items will be displayed
        first in the list and will include a large image,
        title, a short teaser and a read more link.'
),
      '#default_value' => $bean->items_per_page['large_image'],
      '#size' => 5,
    );
    $form['items_per_page']['small_image'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of items with a small image'),
      '#description' => t('These items will be displayed
        second in the list and will include a small picture,
        title and short teaser.'
),
      '#default_value' => $bean->items_per_page['small_image'],
      '#size' => 5,
    );
    $form['items_per_page']['listing'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of items in the listing'),
      '#description' => t('These items will be displayed
        last in the list and will include only a title.'
),
      '#default_value' => $bean->items_per_page['listing'],
      '#size' => 5,
    );

    $form['more_link'] = array(
      '#type' => 'fieldset',
      '#tree' => 1,
      '#title' => t('More link'),
    );
    $form['more_link']['text'] = array(
      '#type' => 'textfield',
      '#title' => t('Link text'),
      '#default_value' => $bean->more_link['text'],
    );
    $form['more_link']['path'] = array(
      '#type' => 'textfield',
      '#title' => t('Link path'),
      '#default_value' => $bean->more_link['path'],
    );

return $form;
}
}

The form method is based on the Bean object at its current state. New beans are initiated with the defaults defined in the values method. This is very useful because you know that value will always be there; there is no need to check for its existence. You can access the properties defined in the values method as properties of the object. Since we defined arrays as our properties, they are arrays on our object. This makes organizing data simple.

It also integrates very nicely with fieldsets. When the form is saved, it matched the name of the property in the values method against the $form_state['values], then stores the settings. When you define #tree=1 in the fieldset, it matches up with the array properties we setup in the values method,

Here is another useful example of a form method that integrates with the media module to provide the media selection UI. If you use the media module, you do not have to worry about setting the file entity status in the validation method.

public function

form

($bean) {
$form = array();

  $form['image_fid'] = array(
    '#title' => t('Image'),
    '#type' => 'media',
    '#description' =>
      t('The Uploaded image will be displayed.'),
    '#default_value' => isset($bean->fid) ?
      (array)file_load($bean->fid) : '',
    '#media_options' => array(
      'global' => array(
        'types' => array('image'),
        'schemes' => array('public', 'http'),
      ),
    ),
  );

  $options = array(
    'imagelink_150' => 'Scale to 150px wide',
    'imagelink_235' => 'Scale to 235px wide',
    'imagelink_320' => 'Scale to 320px wide',
  );

  $form['image_style'] = array(
    '#type' => 'select',
    '#title' => t('Choose an Image size'),
    '#options' => $options,
    '#default_value' => $bean->image_style
  );

  $form['image_text'] = array(
    '#title' => t('Text'),
    '#type' => 'textarea',
    '#default_value' => $bean->image_text,
  );

  $form['more_link'] = array(
    '#title' => t('More Link'),
    '#type' => 'textfield',
    '#default_value' => $bean->more_link,
  );

return $form;
}

Render and Display

Finally we tackle the render of the block. The view method in the plugin should only take care of the data aspect of the rendering. Be sure to use proper theme functions. The default view method expects you to return an render array.

Here is what we are using for the listing block type:

class

listing_bean

extends

bean_plugin

{
public function

view

($bean, $content, $view_mode = 'full', $langcode = NULL) {
$count = $bean->items_per_page['large_image'] + $bean->items_per_page['small_image'] + $bean->items_per_page['listing'];

    $query = new EntityFieldQuery();
    $query
      ->entityCondition('entity_type', 'node')
      ->entityCondition('bundle', 'article')
      ->propertyCondition('status', 1)
      ->propertyOrderBy('created', 'DESC');
      ->range(0, $count);

    if (!empty($bean->filters['term'])) {
      $query->fieldCondition('field_article_type', 'tid', $$bean->filters['term']);
    }

    if (!empty($bean->filters['topic'])) {
      $query->fieldCondition('field_topic_term', 'tid', $bean->filters['topic']);
    }

    if (!empty($bean->filters['audience'])) {
      $query->fieldCondition('field_audience_term', 'tid', $bean->filters['audience']);
    }

    $result = $query->execute();

    if (empty($result)) {
      $content['nodes'] = array();
    }
    else {
      $content['nodes'] = node_load_multiple(array_keys($result['node']));
    }

    $content['#theme'] = 'bean_list';
    $content['more_link'] = array(
      'text' => $bean->more_link['text'],
      'path' => $bean->more_link['path'],
    );
    $content['items_per_page'] = array(
      'large_image' => $bean->items_per_page['large_image'],
      'small_image' => $bean->items_per_page['small_image'],
      'listing' => $bean->items_per_page['listing'],
    );

return $content;
}
}

The view method is passed 4 arguments.

  • bean: The bean object that you are rendering.
  • content: The current content array. If you have fields attached this block type (bundle) then the rendered field will be in this array.
  • view mode: The bean entity view mode that is being rendered.
  • langcode: The language being rendered.

For this block type we want total control over what is rendered so we ignore the current content array. We use EntityFieldQuery to query entities. It's very easy for us query fields that are attached to entities without knowing the table structure. For more information about this, see the documentation for the execute method.

We aren't doing a bunch in this method. Basically just loading the nodes based upon the selected criteria and passing on other settings to the theme layer. The details of the theme functions are beyond the scope of this post, but we will look at them in a future post.

Giving Power to the Editor

All this gives the editor power to create her own blocks for querying content, without needing to deal with a relatively complicated interface.

This is just one scenerio for the use of Beans. For Energy.gov, we created more then ten different block types. Each of these types have a different purpose and an individual instance can be placed anywhere a block can. Stay tuned for how we created the layout of the blocks on the individual pages, and how we themed the output without resorting to custom template files.

Sep 07 2011
Sep 07

Views is an amazing module. The power it provides to build lists of content from within the UI is amazing. The plugin architecture is complicated but extraordinarily powerful. There are currently 125 Drupal 7 modules that extend the functionality of Views. So why didn't we use it for development of Energy.gov?

When we started development on Energy.gov, we took a step back. This would be our first Drupal 7 project. We took this opportunity to reconsider our development practices, and looked at every module we had been accustomed to using and asked two questions:

  • Does it provide a robust API (i.e. can we use it without the UI)?
  • Do we really need it? Are we using the module just because that's what you use when you create a Drupal site? Are we we pushing the module beyond it's original intent? Does this module try to solve too many problems?

Ok, so the second question is really 4 questions. In the case of Views, we could answer "yes" for the first question, we couldn't for the second set of questions. There was no specific requirement for a visual query builder. In past projects, we often spent as much time investigating quirks about how Views (or any other large module) works as we do in custom development.

In the end, one argument against using Views for our content queries overrode all others: we wanted our client to use Views. Our client had specified that once they received the sites, their own developers would be using Views to build blocks and pages themselves. We knew that if we worked in Views for our own work, the Views we created would eventually be exposed to them, which leads to possibilities of regression and error. We wanted our core querying functionality to continue to function without concern that it might be tampered with. We explained our concerns and our proposed approach to the client, and they agreed to it.

So, what would be used in its place? In short, the answer is EntityFieldQuery. EntityFieldQuery is a class, new to Drupal 7, that allows retrieval of a set of entities based on specified conditions. It allows finding of entities based on entity properties, field values, and other generic entity metadata. The syntax is really compact and easy to follow, as well. And, best of all, it's core Drupal; no additional modules are necessary to use it.
A typical EntityFieldQuery that looks up the 5 most recent nodes of types article, page, or blog created by the current node's author, that are published (a typical "more by this author" query) might look like the following:

// get the current node; we're assuming for this example we know we are on a node.
$node =

menu_get_object

();$query = new

EntityFieldQuery

();
$query->entityCondition('entity_type', 'node')
->propertyCondition('status', 1)
->propertyCondition('type', array('article', 'page', 'blog'))
->propertyCondition('uid', $node->uid)
->propertyOrderBy('created', 'DESC')
->range(0, 5);
$result = $query->execute();

Note a few things here:

  • All methods of the EntityFieldQuery class generally chain; that is, they return the modified EntityFieldQuery object itself. You may be familiar with this sort of construct from jQuery.
  • Like Drupal 7's query building in general, the methods of EntityFieldQuery have default operators that they assume, and are fairly agnostic in terms of what sorts of values they'll accept. So for example, propertyCondition() has either '=' or 'IN' as its default operator, depending on whether you pass it a string/number or an array as a comparison value. Of course, if you want a different comparison, i.e. '<>' or 'NOT IN' or such, you can pass that in explicitly.
  • Note that we're not querying fields just yet. EntityFieldQuery really starts to shine when you start querying field values, because it takes care of finding the appropriate field table and doing joins for you.

EntityFieldQuery is powerful, but it didn't do everything we wanted it to. We used Organic Groups on this site, and we wanted our queries to be aware of groups without adding that each time. Also, in almost all cases we were querying for nodes which were published, and we often wanted a reverse chronological ordering. Fortunately, EntityFieldQuery is a PHP class, so it is very easy to extend. We created EnergyEntityFieldQuery.

class

EnergyEntityFieldQuery

extends

EntityFieldQuery

{
/**
   * apply some defaults to all instances of this object
   */

public function

__construct

() {
$this
// we're interested in nodes
->entityCondition('entity_type', 'node')
// Default to published
->propertyCondition('status', 1)
// default to reverse chronological order
->propertyOrderBy('created', 'DESC');

    /* make assumption that we want group content; see method below */
    $this->setPrimaryAudienceCondition();
  }

  /**
   * Helper function for querying by topic vocabulary terms.
   * Will do lookup from term names as a convenience; tids are also recognized.
   *
   * @param $topics
   *    String, numeric or array; converts to array if necessary
   */

  public function setTopicCondition($topics) {
    $topics = !is_array($topics) ? array($topics) : $topics;
    if (count($topics)) {
      // if a term is not numeric, do a lookup for each term and replace it with its tid
      foreach ($topics as $idx => $topic) {
        // try to find a tid for non-numeric terms
        if (!is_numeric($topic)) {
          // look it up
          $vocab = taxonomy_vocabulary_machine_name_load('topics');
          $candidate_terms = taxonomy_get_term_by_name($topic);
          foreach ($candidate_terms as $candidate) {
            if ($candidate->vid == $vocab->vid) {
              $topics[$idx] = $candidate->tid;
            }
          }
        }
      }
      // field_topic_term is our term reference field for the Topics vocabulary
      // once we have converted all our terms to tids, we set them as a field condition for our search
      $this->fieldCondition('field_topic_term', 'tid', $topics);
    }
    return $this;
  }

  /**
   * Add the field condition to search by the primary audience field.
   * EnergyEntityFieldQuery makes the assumption that we want content that matches the current group.
   * The class will provide an undo method
   *
   * @param $gid
   *   An array or integer for the gid(s) to search the primary audience field
   *   based on. If empty, will try to pull current group from the page context.
   */

  public function setPrimaryAudienceCondition($gid = NULL) {
    if (empty($gid)) {
      $current_group = og_context_determine_context();
      $gid = $current_group->gid;
    }

    if (!empty($gid)) {
      $this->fieldCondition('group_audience', 'gid', $gid);
    }
    return $this;
  }

  /**
   * Unset group content conditions
         *
         * Use this method if you do not want to filter by group content.
   */

  public function clearAudienceConditions() {
    foreach ($this->fieldConditions as $idx => $fieldCondition) {
      $field_name = $fieldCondition['field']['field_name'];
      if (($field_name === 'group_audience') || ($field_name === 'group_audience_other')) {
        unset($this->fieldConditions[$idx]);
      }
    }
    return $this;
  }

  /**
   * If we're currently on a node, and if the entity_type is node, exclude the local node from the query.
   * This prevents the node the user is viewing from showing up in queries.
   */

  public function excludeNode($nid) {
    if (!$nid) {
      $object = menu_get_object();
      $nid = $object->nid;
    }
    if (!empty($nid) && $this->entityConditions['entity_type']['value'] === 'node') {
      $this->propertyCondition('nid', $nid, '<>');
    }
    return $this;
  }

}

Note that it is also possible to override the protected methods of EntityFieldQuery itself. This may be useful, for example, if you have more complex propertyCondition needs than EntityFieldQuery itself provides.

We found that we were able to solve around 90% of our content listing use cases with EnergyEntityFieldQuery. We build a simple UI to allow users to pass parameters to the class, which allowed them to easily create dynamic query.

Let's run through a simple example of displaying a list of nodes. Remember that this is not sample code. This is real code we are using on Energy.gov.

The easy part. Remember using EnergyEntityFieldQuery allows us to restrict our queries to OG group content and sets the default entity type to node.

$query = new EnergyEntityFieldQuery();

We only want nodes that are rebates.

$query->entityCondition('bundle', 'rebate');

EntityFeildQuery has built in paging and table functionality which is trivial to add.

$query->pager(10);

We are pull conditions from the query string to create taxonomy filters.

function energy_rebate_savings_search($filters = array()) {
// This maps the vocabulary machine names to the field names for term references to that vocabulary.
$term_field_map = array(
  'rebate_provider' => 'field_rebate_provider',
  'rebate_savings_for' => 'field_rebate_savings_for_short',
  'rebate_eligibility' => 'field_rebate_eligibility_short',
);
// Get the non 'q' parameters
$params = drupal_get_query_parameters();
$param_filters = array();
foreach (array_keys($term_field_map) as $vocab) {
  if (isset($params[$vocab])) {
    $param_filters[$vocab] = $params[$vocab];
  }
}
$filters = array_merge($param_filters, $filters);

Set the condition for the terms if there are any

foreach ($filters as $filter => $value) {
  if ($value != 0) {
    $query->fieldCondition($term_field_map[$filter], 'tid', $value);
  }
}

Run the query

$result = $query->execute();
// process and theme the results, not part of this code example
}

This returns to us an array of entity ids (nids in this case) that match the conditions we've given it. Once we have those, we can process them any way we like. Our preferred method is to use Drupal 7's greatly expanded concept of view modes, which we will talk about in a future post in this series.

So, this is all well and good. But how do we get these to display? How do we place them? One of the advantages of Views is that it can provide blocks of your constructed query which become available to the system immediately.

The answer to that, for Energy.gov, is Beans. Bean is a contributed module we developed in the course of this project which creates blocks as entities. In the next post, you will learn how we used the Bean module to replace View blocks, and how Beans can be used to do much more.

Aug 09 2011
Aug 09

My first ever Fuse blog post will focus on the Context module developed by the DC based Development Seed. With 29577 reported installs of the module, Context is quickly climbing the module ranks. It's already part of our base install for all sites we work on here at Fuse. 

Simply put, Context lets you determine specific reactions on a set of conditions. On every page load, it checks to see if any active contexts have conditions that have been fulfilled, and if so, it performs the reaction. To show you how it works I will give you an example of what can be achieved with Context.  In this example we want to create an active menu trail for content tagged with a specific taxonomy term. That taxonomy term will be your condition and the reaction is the desired active menu trail.  Here are the steps to take to make this work: 

1. Install Context:

As of today, the latest version of context is 7.x-3.x, which is not that different from the version 6.x-3.0. I will be working with Drupal 7 version since we're using D7 for all our new builds at Fuse. Install Context the usual way just don't forget CTools, as it is a dependent module. In Drupal 6.x environment you will also need the jQuery UI module which provides you with an admin interface for some extra features. (D7 has the jQuery included within core)

2. Add a new context:

Under Structure > Context you’ll get a list of all the contexts you've created and a search bar. You should be looking at an empty list after installing the module.

On top you can +Add or +import. Lets add a new context for now (we’ll get to importing a bit later.) Adding a new context will prompt you for Name, Tag a description. The "Tag" field will be used to group contexts on the context listing page.

3. Set your conditions:

This is where you will set the various conditions for your context. As mentioned above, conditions are checked on page load, and if the condition is met, the configured reactions are performed. Context comes built in with quite a few default conditions that will probably, for the most part, fulfill your needs. However Context is fully extendible and there are already modules out there that provide new and exciting conditions and reactions. This extendibility is discussed further at the end of this post. For now, we'll just go over the default conditions:

Context: The condition is met, if another context's conditions are met. Perfect for recycling your already set context, if there are currently active contexts that you would like to base your new context on, the context option would be perfect for it. I hardly ever duplicate the exact same condition set between two or more contexts, but there is the odd time when I like to use a context I have already set and fine tune it (ie. create another condition on top of it).
Menu: Allows you to select any number of menu items. The condition is met when any of the selected menu items belong to the current active menu trail.

Node Type: Select from a list of node types. The condition is met when viewing a node page (or using the add/edit form -- optional) of one of the selected content types. 

Taxonomy: Your condition is met if the current node being viewed is referring to a particular taxonomy term. Don't confuse this condition withTaxonomy term.

Path: Allows you to supply a list of paths. The condition is met when any of one or more paths match the supplied paths.

Site-wide Context: The condition is met at all times.

Taxonomy term: Will set the context when viewing the taxonomy term's page (not a node that is referring to that taxonomy term).

User Role: The condition is met if the current user has one of the selected role(s).

User Page: Lets you choose from a list of 'User' pages. (i.e. User profile, User account form, Registration form). Condition is met when viewing the selected pages.

Views: This option will list all active views and their specific generated pages. This allows you to trigger your context for any pages that a particular view is active on.

4. Set your reaction:

Once your conditions are set, it's time to set up your reactions. Once again, we'll just go over a few of the reactions that comes with Context built-in:

Blocks: The blocks reaction is probably my most used reaction of all. It allows you to place any block in any region when the condition is met. This provides a much more flexible way to add blocks to the page than the blocks administration page (admin/structure/block) since you can use more than just the path as the criteria for when a block should be visible or not.
 

Note: 

There is one tricky thing when using Context to place your blocks and that is the ordering of the blocks within a particular region. Within a context, it's easy to reorder the blocks within a region using the standard drupal drag and drop interface. However, If you have two

different

contexts adding blocks to the same region you will need to order them manually. Under the "+add" in the region header, click the 

 icon and the weight field will appear. Here you can assign a specific weight number to your block. The weight will be respected accross all contexts so you just need to make sure the blocks you want to appear first have lower weights than ones you want to appear after.


By drag and drop sort method vs. weight select sort method:

 

Breadcrumb: Set the breadcrumb trail to a particular menu item.

Menu: Set the Menu Active class

Theme Page: Override the section title and the section subtitle of the page. This will also override your $section_title and $section_subtitle variables within your page.tpl.php.

Theme html: Add an additional html body class to the page

5. Import / Export:

You can easily export an Context by clicking on "Export" (under Operations) on the Context listing page.

The result will be a block of text that can be copied and then imported back to another site. Just select "+Import" from the top (next to the "+Add" button) and paste the exported text. Hit save and you will have an exact copy of the context.

6. Context Editor:
 

Having the Admin menu module installed, there is the handy context editor window for testing and editing contexts. Active contexts are easily detected and can be modified on the fly by adding conditions, blocks (drag and drop) and theme variables.

7. Book keeping:
 

Usually on substantial projects the Context overview list gets messy and a bit confusing. When there are a lot of contexts it can be hard to find the one that is outputting a certain block on a certain page. To avoid this confusion I recommend a few things:
 

  • Write Descriptive Descriptions! It sounds redundant, but the better your description is, the easier it will be to figure out which context is outputting "that block" in "that region" on "that page".
  • Use Tags Wisely! Tagging can be very useful since the contexts on the context listing page get grouped by tag. If you group your contexts intuitively using tags, you'll spend less time finding your contexts and more time trying to figure out if we're ever going to get multigroups back.
     

8. Extending Context using the API

As mentioned above, Context comes with an API to extend it's functionality by adding custom conditions and reactions. An example of one of these modules is Background Images (built by Fuse's own Chris Eastwood). It provides a new reaction that can change the background image of any css-selectable element on the page. While this tutorial will not delve into how to use the API to extend context (perhaps in another tutorial down the road?), I thought it was worth mentioning in case you need a condition or reaction that isn't built-in. You know how it often goes with Drupal, if it's not built-in, there may just be a module for that! 

Jul 15 2011
Jul 15

Recently, I blogged about Drupal's plans to integrate HTML5 into its next release. However, version 8 of Drupal is at least a year away. What's a Drupal dev to do, if they want to start using HTML5 right now? There are a variety of tools and techniques. Let's look at them.

Drupal 6 or Drupal 7 – Does it matter?

In a word, “no”. In fact, if all you want to do is use HTML5 elements on your website, you don't really need to use the Drupal tools – themes and modules – to use HTML5 on your site. All you need to do is change the doctype in your theme's page.tpl.php file from:

to

html.

Simple, eh? You can now use HTML5 tags like “<video>”, “<audio>”, “<canvas>”, etc., on your pages. For a full list of HTML5 tags and their browser compatibility see: http://caniuse.com/#cats=HTML5. If you are a more visual person, you might prefer this interactive graph by HTML5's Superman, Paul Irish.

Modernizr

Better yet, use Irish's modernizr, “an open source, MIT-licensed JavaScript library that detects (browser) support for many HTML5 & CSS3 features” (http://diveintohtml5.org/detect.html). There's a module, available for both Drupal 6 and 7. If you prefer, you can build your own modernizr script, choosing just the elements you want to use to keep your script small, download your customized library, and add it to the head of your theme's page template files with:

<script src="http://civicactions.com/blog/2011/jul/15/using_html5_with_drupal_today/(path-to)modernizr.min.js"></script>

In Drupal 6, this would be page.tpl.php and in Drupal 7, html.tpl.php. Read the full instructions here.

HTML5 Sectioning Flowchart

Semantic Elements

If you want to use HTML5 semantic elements, you do not necessarily need an HTML5 theme. You can manually replace the CSS “divs” you want to change, in your various template files, with their corresponding HTML5 elements: “<div id=”header”>” with “<header>”, “div id=”main”>” with “<section id=”main”>”, etc. Each theme will likely vary slightly, in how it implements HTML5 semantic elements. If you compare the two Drupal 6 themes, HTML5 Base and Boron, for example, you'll see that Boron turns each “block” into a “section” while HTML5 Base leaves “block” elements as “blocks”. See the handy flowchart from HTML5doctor.com, and this excellent post, also from HTML5doctor.com, for instructions on how and where to use the new semantic elements. For a more indepth discussion of HTML5 semantic elements, see http://diveintohtml5.org/semantics.

HTML5 Shiv or Shim

Last in the process of adapting your own theme for HTML5 is linking to Remy Sharp's HTML5 enabling script. What this does is get Internet Explorer to recognize HTML5 elements. All you need to do is put the following in your “<head>” element:

<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->

Tada! You've rolled your own HTML5 theme!

Once you are done, check your work via the HTML5 Outliner.

Drupal HTML5 Themes

There are many good HTML5 Drupal themes that, with the exception of implementing modernizr, have done all of the above work for you. If you want to reap the benefits of Open Source community work, use one of them – and apply the time you save bettering it, or working on some of the many modules in development for HTML5.

Currently, there are almost 600 themes for Drupal 6, and almost 200 for Drupal 7. Of these, for Drupal 6, 17 reference “HTML5”, and for Drupal 7, 20. Almost all of the HTML5-referencing themes are starter themes; that is, they are not designed to be used as is, but as bases from which to build your own theme.

Drupal 7 Themes

The most popular Drupal 7 HTML5 theme is Sky. I suspect it is most popular because it's been around, in one form or another, since 2007. I'm not saying it's not a good theme! I've used it, for Drupal 6. It's clean, attractive and can be used as is, or as a base them. It's currently maintained by the Drupal HTML5 initiative leader, Jacine.

The second most popular is AdaptiveTheme (as of today, Thursday, July 7, 2011 – yesterday, the second most popular was Omega), which, despite the name, does not in fact, appear to be “adaptive”. “Adaptive” or “responsive”, in regard to web sites, generally refers to the use of CSS3 media queries to assign different stylesheets for different sized viewports and devices. Version 7.x.2.x of AdaptiveTheme, according to this page, will incorporate media queries, but until then, there is no AdaptiveTheme Mobile sub-theme for Drupal 7. If you want to use media queries, nothing is stopping you from adding them yourself. AdaptiveTheme includes features: rounded corners settings, RDFa and microformat compatibility, “Gpanels which are drop in snippets for creating multi-column layouts (such as a 5 column footer)”, and optional settings for things like: adding “extra CSS class names via theme settings” and “SPAN elements to menu anchor text”.

Media Queries

For an excellent tutorial on media queries, see this Web Designer Wall post. Here's a simplified example of media queries in action, from another Web Designer Wall tutorial, and here's a fine collection of sites using media queries. Oh, and here's the anti-media-queries argument; namely, that media queries do not strip out large images and scripts, thus rendering your web site too large and slow for mobile devices. The answer to that, however, is to use a bit of Ye Olde Javascript.

Framework simply uses HTML5 structural markup, while Genesis is only HTML5 friendly in the latest development version.

The now fifth most popular, Omega, is interesting. Firstly, it is an “adaptive” or “responsive” theme using media queries. Secondly, it integrates with a module, “Delta, originally designed to only work with it, that, in concert with another module, Context, allows you to use “different layouts/settings for various portions of a site”. Lastly, another module, like Delta, originally designed for use only with Omega, Omega Tools, allows you to quickly develop a sub-theme using Drush. Omega is rather large: 1.1MB! You could probably throw out all but the base folder and/or a starter kit, bringing the size down to about 350K, however.

Gamma and Beta are both Omega sub-themes.

Boron is a minimalist theme based on the popular Zen. Boron also comes in a Boron for Zen flavour. Boron for Zen is only for Drupal 6.

Panels 960gs (HTML5), Zentropy and MinimalistTheme are all Boron-based. Panels 960gs incorporates the 960 Grid System and Panels module. Zentropy is based on Paul Irish's HTML5 Boilerplate, meaning it is optimized for performance and mobile, among other things you can find out about here.

LayoutStudio's HTML5 version is in process, while HTML5 Base, a promising theme by Drupal 7 base theme designer, Jen Simmons, is still only available in a development version.

Plink, like HTML5 Base, looks interesting but is still only available in a development version. Plink comes with media queries, modernizr, “baked in” LESS and a “jQuery Mobile default sub theme for use in mobile switching”.

Drupal 6 Themes

If you want to take advantage of the almost 6000 modules that exist for Drupal 6, you might want to forego Drupal 7 for now. Though it appears that there are 17 HTML5 themes for Drupal 6, many are only HTML5 for Drupal 7, or merely reference the term in their write ups. Sky, for example, is only an HTML5 theme for Drupal 7; likewise, AdaptiveTheme, Genesis and Layout Studio. Framework, Boron, Drixel 5, HTML5 Base, modernbird, Boron for Zen and Roots are HTML5. *Whoops, looks like as of today, HTML5 Base is no longer available for Drupal 6, but is available for Drupal 7; only as a development version, though.

Roots is based on HTML5 Boilerplate and appears to be well thought out.

There are many good HTML5 starter themes for both Drupal 6 and 7. It's also easy to adapt your own theme for HTML5, or to adapt an existing theme for your specific needs. There does not seem to be a strong front-runner in the HTML5 starter theme arena, comparable to the popular Zen. What HTML5 theme are you using or are you interested in? Why?

Modules

For Drupal 6, 26 modules reference “HTML5”, and for Drupal 7, 22. Many of these modules are still in development or not yet available. Let's look at what we have.

Video is the most popular “HTML5” module for both Drupal 6 and 7. It's been around (sans HTML5) since 2005. Video allows users to “upload video in any format, play video in any format “ and transcode video to the HTML5 friendly h246, Theora and VP8 codecs.

Second up is Media: Vimeo. This module extends the functionality of Embedded Media Field to allow users to embed videos from the Vimeo video service, “using HTML5 when supported”. Media: Archive provides similar functionality for audio and video served from Archive.org. The Drupal 6 version of Media: Archive doesn't appear to support HTML5. The Drupal 7 version has some HTML5 support and plugs into the Media module.

Third most used is Elements, which provides a library of form elements: tableselect, emailfield, searchfield, telfield, urlfield, numberfield and rangefields, that a developer can use in their own modules. (Note that the Drupal 7 version does not include tableselect element - this functionality is available via the Drupal 7 core.)

HTML5 Tools is a diverse set of tools to help developers HTML5-ify their site. You need to install Elements to use HTML5 Tools, whichg is comprised of itself plus a sub-module: HTML5 Forms Tools. Forms Tools overwrites Drupal default forms with their HTML5 counterparts – or will do that. Currently, only email elements on user settings and site configuration pages, the search element on the Search Module, and the search element on the Search Theme Form are ready. Follow the progress of this module here.

Plupload Integration creates multiple file upload capabilities, and utilizes an HTML5 widget if the client computer supports it.

MediaFront is a media player who's appearance can be customized without writing code. It utilizes the Jquery Open Standard (OSM) Media Player, which plays many types of media.

JPlayer integrates the jPlayer JavaScript library with CCK file fields and Views. The jPlayer JavaScript library “provides an HTML5-based player, that uses a Flash fallback.”

VideoJS (HTML5 Video Player) is another HTML5 video player that utilizes an HTML-based solution to video display: Video for Everybody, and javascript, to cover an optimal number of browsers and devices.

There are HTML5 modules, and adjustments to existing modules, in development for GeoLocation (I know, I know, not strictly HTML5!), Canvas, processing.js and websockets. There are several graphing modules in development: Visualize, GraphAPI and Rgraph. There's an HTML5 image editor module ready to go. To see all that is available for HTML5, go here.

It's an exciting space: HTML5 and Drupal. As we've seen, we don't really need HTML5 specific themes and modules to at least get started using HTML5 on our Drupal sites; however, there are many available to help us, and many more in active development. As always, along with having fun, remember to join in: create, share and help develop the modules and themes you want to see!

Feb 10 2011
Feb 10

on 10 February, 2011

Here are the slides from the short talk I did on Development Seed's excellent Features module, at the February Drupal-Drop In, hosted at Microsoft's offices in London.

It was a great evening, where several people did a short talk on their favourite modules, or modules that they find themselves using all the time. Thanks to everyone who came along! For those who missed it, there's a good re-cap over at UBelly.

Jan 27 2011
tim
Jan 27

I first came across the FullCalendar jQuery plugin while searching for a better way to mimic the features and style of Google Calendar. I was immediately impressed; it combined features like drag-and-drop and dynamic resizing with the limitless flexiblity of an open-source project.

Whenever I come across a new technology or service, the first thing I do is check to see if there is a Drupal module available. As is generally the case, there already was a Drupal module released, though only as a beta. Only the most basic features had been implemented, so I dove headfirst into the issue queue, submitting feature requests and filing patches. After several weeks of enthusiastic back-and-forth, the maintainer decided to turn the project over to me, and focus on his freelance work.

While at Zivtech, the FullCalendar module has flourished, with the addition of real-time drag-and-drop, event resizing, and translations. The module now has stable releases for both Drupal 6 and Drupal 7, and work has begun on the next major release for each version of Drupal. Yet to come are features like improved accessibility, integration with public Google Calendar feeds, and fully customizable event color-coding.

The standard Calendar module has long been a source of frustration for users of Drupal, and is not yet available for Drupal 7. Adoption of FullCalendar has risen steadily with the release of Drupal 7, as the ease and simplicity of FullCalendar makes it a welcome addition to any site.

Jan 12 2011
Jan 12

Previous articles have discussed the conceptual groundwork and setup of mobile sites in Drupal. It’s now time to look at a number of themes and modules which will help you in your endeavours. We’ll also cover a number of custom fixes which can be made to the HTML, CSS and JS in your site.

Mobile themes

Funnily enough, the selection of the mobile theme is looking to be one of the least important technical consideration with the whole mobile site. It’s one area where I am not in the best position to comment on the various merits of themes as I haven’t really tested theme all. I went with Nokia Mobile because it seemed to have a solid base, being based on code developed by Nokia. That said, I did have to make a number of changes to it to get it to work in with my site. Be prepared to get your hands dirty with page.tpl etc. The Adaptivetheme Mobile theme looks quite promising, being a sub theme itself it would naturally fit well with a desktop theme derived from the same base.

Nokia Mobile “Provides different presentation layers to best serve basic devices and high-end smartphones.” Mobile Garland Garland inspired mobile optimized Drupal theme intended to be used with a mobile optimization module Mobile Plugin. Adaptivetheme Mobile Hurrah! A mobile sub theme. “Adaptivetheme Mobile is a subtheme for Adaptivetheme. It is designed purely to build Drupal mobile themes for mobile web devices (for mobile websites).” Mobile “intended to return only clean HTML with no styling (images and styling in content is maintained) .mobi “Display a mobile, portable format.”

Mobile modules

There are a lot of options available to you when it comes to deploying modules to help you with your task. I am very much of the mind that modules should only be deployed if they are fit for the task and don’t introduce too much overhead or code you don’t understand. My aim is to keep things as “pure” as possible. In many cases you may be better writing your own custom code if you feel comfortable doing that.

Many tutorials recommend going with Domain Access and Mobile Tools with Browscap. It is a combination which could work well for you. However, I ended up not deploying any of these modules, chosing to go my own way. I’ll walk through each of the modules, their main features and why I went the way I did. It basically boiled down to the fact that Apache, settings.php and my own custom tweaks got me most the way there.

Domain Access

Domain Access is a popular suite of modules which can be used to manage (sub) domains for a (mobile) site. It is exceedingly well documented and structured. It looks to be a high quality module which supports a lot of functionality. Many mobile tutorials speak highly of it and recommend it for mobile sites.

Knowing relatively little about the module I reviewed its main features to see what it had to offer a mobile installation. From my quick review I have been unable to find anything compelling for the problem set I was facing. That said, if your mobile site is to diverge significantly from the desktop site you may find that some of the customisation features quite useful. There may well be stuff that I am missing and would be happy to be enlightened. The relevant features are as follows:

  • Domain Access: The core module allows for (sub) domains to be registered. This is really just the basic setup for the modules. In order for this to work your DNS and VirtualHosts need to be set up as you normally would for a multisite. ie. each domain pointing to the IP of your Drupal installation.
  • Domain Alias: It is possible to define domain aliases for each registered (sub) domain. eg www.example.com -> example.com. Alternatively, this result could be achieved by adding some aliases in you VirtualHost section in Apache.
  • Domain Theme: Allows you to define a theme for each (sub) domain. Alternatively, if you were using a multisite setup (or some conditional logic) you could set the default theme in settings.php.
  • Domain Config: Offers an extensive set of site configuration options including email, slogan, mission, footer, frontpage, anon user, admin theme, time, cache, menu mappings. Most of these tweaks can be achieved by traditional means. Conf variables can be overridden in settings.php. Custom menu blocks can be placed into regions.
  • Domain Source: Source domain for linking to content. This ensured that some links are rewritten to point to main site. In a mobile setup you would want the site to operate as normal (no rewriting). The duplicate content can be fixed with canonical URL link in the head.

Mobile Tools

Mobile Tools is a popular module which has a bunch of handy utility features most mobile sites could use.

  • Detection of UA and redirect based on Browscap and WURFL:Possible to map user agents to themes. More sophisticated if Browsercap or WURFL is used. This redirection should be taking place outside of PHP so I am a fan of doing this in Apache rewrite rules or maybe even a caching/reverse proxy layer. This alternative approach has been discussed above.
  • Block telling users of Mobile site: Helpful but easy to add manually.
  • Panels integration: No doubt very helpful if you are using Panels, as Panels own a path and that’s it. This could be a deal breaker so this could be essential. Personally, I stuck to very simple design so Panels wasn’t an issue for me.
  • Permissions integration: Mobile roles can be used to turn block aspect of the site based on permissions. This is a really good idea and a neat way to turn stuff off.
  • Change number of nodes on homepage: Helpful but could be done with a different view exposed as a block.
Drupal Support for Mobile Devices [Rachel Scott] Overview of the Mobile Tools module with screenshots. Mobilize Your Drupal Site with Mobile Tools Overview of the Mobile Tools module describing multisite setup.

Mobile Plugin

Wide range of features. Tackles some practical isses such as word breaks, scaling images, video embedding, filtering JS. Does device detection and provides own mobile theme. Unfortunately the doc specifies that “Page caching must be off to support multiple themes!”. This warning would put most people off. Does this apply even if two sites are being used?

Browscap

A popular module which returns capabilities based on user agent. The module will fetch updates to a database of browser user agents. Integrates with Mobile Tools.

WURFL

“The WURFL module helps you in detecting the device capabilities of the mobile device visiting your website.” Integrates with Mobile Tools. Knowing the capabilities of a device at a very fine level of granularity could be helpful if you are into eeking out every enhancement you can. the question is whether you need this level of control.

Module code vs Theme code

Adding a mobile version of your site will make you think about code duplication issues. If you have custom logic in your theme for the desktop site then there is a pretty good chance that a large chunk will be copied across to the mobile site. Bad news. Much of what makes it into themes is not 100% concerned with presentation. It’s hard to draw a line but if the code is required for mobile and desktop then it is a good candidate for being factored out into a more central place such as a module. Less code means less work for you in the future. If you do have custom code in template.php then take a look through it and see what can be moved.

Custom content

Not all changes can be made in the theming layer, it will be necessary to change and optimise the content served.

Custom blocks

Managing block configuration (region, order, title, paths, permissions, etc) is a right royal pain in the you know where, especially if you have a lot of blocks and you need to deploy across dev, staging and production. Going into the blocks admin interface and moving stuff around, editing, saving and repeating gets old real quick. Configuration concerns such as this have been overcome largely though code from DevelopmentSeed. Features to hold logic and configuration for grouped functionality. Features work nicely together with Context, which allows for Blocks to be positioned according to an overarching context. Cool. Context could be the answer we are looking for. It certainly is for a normal desktop site.

However, when it comes to configuring blocks for a mobile site, Context only knows about the current theme. This is a known issue for Context. There is another module, called Features Extra which possibly offers a way to capture config info for blocks, however it too suffers with themes. AFAICT it still isn’t possible to capture block config with multiple themes. Bummer. I’d be interested to know if there are solutions here.

In the meantime you can manually configure blocks the old school way but it really isn’t ideal.

Custom modules

This is one area I was unable to nail as well. In a few places it would have been very handy if I could have turned off a module dynamically to make the mobile site a bit simpler, eg. colorbox, admin menu. AFAICT there is no way to do this. Tracing the calls during bootstrap, I see that module_load_all() is called very late in the procedure at _drupal_bootstrap_full(). module_load_all() calls module_list() which gets all active modules. It would be great if module_list() could look to conf variables to respect a stop filter of modules. Not going to happen I know, but would be handy.

This is where the permissions integration in Mobile Tools could really shine. Instead of disabling a module you could control the operation of a module via permissions. Most modules should have permissions limitations on functionality and so can be turned off for the mobile site.

One way to work around this is to mop up the HTML/JS/CSS in the theme layer. This approach is ugly, error prone and brittle, but does hold some promise. You will find recipes similar to the following around the traps:

/**
* Implementation of hook_preprocess_page().
*/
function mw_mobile_preprocess_page(&$vars) {
if (!cuf_mobile_is_mobile()) { return; }
// Strips out JS and CSS for a path.; // http://www.mediacurrent.com/blogs/remove-or-replace-jscss-page
// WARNING: The code below messes up jQuery Update even when no scripts are
// replaced. Use at own risk.
$remove_csss = array(
//’colorbox’ => array(‘/styles/default/colorbox_default_style.css’),
);
$remove_jss = array(
//’colorbox’ => array(‘/js/colorbox.js’, ‘/styles/default/colorbox_default_style.js’),
);
// JS
$scripts = drupal_add_js();
if (!empty($vars['scripts'])) {
foreach($remove_csss as $module=>$paths) {
foreach($paths as $path) {
$module_path = drupal_get_path(‘module’, $module);
unset($scripts['module'][$module_path . $path]);
}
}
$vars['scripts'] = drupal_get_js(‘header’, $scripts);
}
// CSS
$css = drupal_add_css();
if (!empty($variables['css'])) {
foreach($remove_csss as $module=>$paths) {
foreach($paths as $path) {
$module_path = drupal_get_path(‘module’, $module);
unset($css['all']['module'][$module_path . $path]);
}
}
$vars['styles'] = drupal_get_css($css);
}
}

In the end I gave up on going down this path because I was running into a problem with jQuery not being updated, leading to JS errors on the page. It was too brittle for me to trust.

For me, the take away is that you are pretty much stuck with using the some modules if you are sharing the database. You just have to be aware of this when designing the site. The only way to solve this is to place some conditional login into you own custom modules which checck for the site being mobile. If you are using contrib then things will be a trickier.

You may desire have custom primary and secondary links for the mobile site. If you really have thought mobile first then maybe the menus will be the same :) but there’s a good chance they will be paired down for the mobile site. It’s not possible to easily define two sets of primary menuas, one for mobile and one for desktop. However, Mobile Tools offers a way to map primary/secondary menus to other menus. There are two other options though if you don’t want to install Mobile Tools.

  • Define different menus (eg. Primary Mobile) and drop them into desired region using Blocks. Comment out the primary links in page.tpl.
  • Programmatically set the links in a custom module

In the end I just programmed these menus in code in my mw_mobile module because the menus had some logic in them for login/logout links:

/**
* Implementation of hook_preprocess_page().
*/
function mw_mobile_preprocess_page(&$vars) {
if (!mw_mobile_is_mobile()) { return; }
// Completely hijack the primary menu and set it from code. This allows
// the primary menu to be managed in features for the desktop site. We just
// need to oveerride it here.
$vars['primary_links'] = array();
$vars['primary_links']['blah'] = Array (
;’title’ => t(‘Blah’),
‘attributes’ => Array(‘title’ => ‘Blah.’),
‘href’ => ‘blah’
);
// etc
}

Custom Views

This section really gets back to the “mobile first” and “responsive web design” concepts we discussed earlier. Views are very powerful and there is a strong temptation to make them as sexy as possible, displaying images, extra content, edit links, star ratngs and the like. Step back and take a look at what you are doing. It maybe possible to design a simple display which works well in mobile and desktop.

Often you really do want to display rich tabular information in the desktop version of the site. In these cases you shouldn’t compromise – you’ll need to create different versions. In these cases progressive enhancement doesn’t really cut it as you want to return more content, not just tweak the presentation.

If it is a View Block which is giving you grief then just make a mobile version and use that instead. Use the block system to place different blocks for the different themes.

If it is a View Page then you could be in trouble as the View takes hold of the path and it is difficult to customise that on a per site/theme basis. One solution is to expose the View as a Block (with a mobile version) and then place that block on a Page (node) or menu path you have made. In this case the page is acting like poor man’s Panels. A bit ugly but it works.

If you are lucky you might be able to define a custom formatter which just returns a blank (or a simple version) if the site is mobile.

A final alternative is to define View templates which have some conditional logic in them. This is possibly the purest way but I think it could become a maintenance issue. We are trying to minimise duplication and effort – creating new files with extra display logic is best avoided.

Custom Panels

I’ll come clean here and own up to not having caught the Panels bug just yet, being content to limit my degrees of freedom. Yes, I am that boring :) Anyway, Panels faces a similar problem as Views Pages in that they are tied to a path which isn’t scoped by a theme (as Blocks are). In this case, Mobile Tools could be quite helpful and showing different layouts for a Panel.

Custom Variables

Drupal can be configured with a whole bunch of variables, many of which are available for editing in the site configuration part of the site. Fire up phpMyAdmin and browse the variable table to get an idea of what is available. These variables are site wide and as such will apply equally to the mobile and desktop versions in our multisite setup. It is possible to override these variables for the mobile site by tweaking settings.php. We have already seem this in action for the default theme. You can do it for other things as well. Mobile Tools offers an interface for this but you can do it manually. I have found that only a small number of rarely changed variables need to be tweaked and so settings.php is a viable option.

$conf = array(
‘theme_default’ => ‘mw_nokia_mobile’,
‘googleanalytics_account’ => ‘UA-xxxxxxxx-2′,
);

Output tweaks

Mobile devices have special requirements, not all of which could be handled by the theme templates alone. The metadata and content of the sites may need some special work. The Rethinking the Mobile Web slideshow above noted that we need to adjust and compress content to make it palettable for mobile. This is where a lot of that nasty work happens. You’ll probably only run into these issues after testing the site for real. No doubt you will have your own special set of problems to deal with. The http://drupal.org/project/mobilepluginMobile Plugin module plugs some of these holes.

ImageCache

You probably have a bunch of ImageCache presets defined for your site, which may or may not be captured in Features config. These presets may be outputting images at a size which is too big for the mobile site. Anything wider than 100px is probably too big. You are aiming to be frugal with bandwidth as well as screen real estate. Time to get shrinking those images. See hook_preprocess_page code below.

Secure Login

If you are using the Secure Login module, you may run into difficulties if you have the multisite setup. The way I had Secure Login configured was to specify the URL to redirect to. This URL is of course for the desktop version of the site and your mobile users will be routed to the desktop site after they log in. They may not notice it if URL redirecting is working for mobile users but we would want to minimise redirects such as this.

It is possible to leave the Secure Login URL blank and then it will apparently use the base_url defined in settings.php. This would be a sensible approach, however, I was having all sorts of path difficulties with ImageCache if I specified these URLs. Don’t know why. Anyway, the easiest solution for me was to stick with the hardcoded URL for Secure Login and then to fix it up in the module.

/**
* Implementation of hook_preprocess_page.
*/
function mw_nokia_mobile_preprocess_page(&$vars) {
// imagcache images are shrunk to mobile size
$fix_regions = array(‘content’, ‘right’, );
foreach($fix_regions as $fix_region) {
_mw_nokia_mobile_fix_imagecache($vars[$fix_region]);
}
// secure login will target the url you entered into the site config.
// there might be a better way to fix this but we just string replace here
_mw_nokia_mobile_fix_secure_login_form($vars['content']);
}

/**
* Secure login hardcodes the URL entered in the config: securelogin_baseurl
* This will be the desktop version of the site. We need to change it to the
* mobile version. There isn’t an obvious way to do this via code, unless you
* write your own hook_form_alter but that would usurp the function of
* securelogin. So we just mop up afterwards. These login pages will be
* cached anyway.
* eg https://example.com -> https://m.example.com
*
* NB: It MIGHT be possible to leave out securelogin_baseurl in the config and
* manually set the base_url in the settings.pgp for the site. However, when
* I did settings like this in the past I ran into problems… can’t remember
* what they were now… So this might be a solution which would avoid the need
* for this code.
*/
function _mw_nokia_mobile_fix_secure_login_form(&$text) {
if (!module_exists(‘securelogin’)) {
return;
}
$sec_url = variable_get(‘securelogin_baseurl’, ”);
if (empty($sec_url )) {
return;
}
$new_url = str_replace(‘https://’, ‘https://m.’, $sec_url);
$pre = ‘<form action=”‘;
$paths = array(‘/user/login’, ‘/user/register’);
foreach($paths as $path) {
$search = $pre . $sec_url . $path;
$replace = $pre . $new_url . $path;
$text = str_replace($search, $replace, $text);
}
}

/**
* Map imagecache presets to smaller presets. This is VERY UGLY because you
* need to correct for the width and height as well. Sorry to have impinged
* upon your senses!
* Adapted from http://groups.drupal.org/node/50678#comment-227203
*/
function _mw_nokia_mobile_fix_imagecache(&$text) {
if (!module_exists(‘imagecache’)) {
return;
}
// mappings. ignore: slider_perview, slider_thumbnail
// old ic, old width, old height, new ic, new width, new height
$mappings = array(
array(‘thumbnail_small’, ’83′, ’83′, ‘mobile_thumbnail’, ’75′, ’75′),
array(‘thumbnail’, ’100′, ’100′, ‘mobile_thumbnail’, ’75′, ’75′), // thumbnail last
);
// fix
$file_url = base_path().file_directory_path();
foreach($mappings as $mapping) {
list($old, $old_w, $old_h, $new, $new_w, $new_h) = $mapping;
$old_path = $file_url .’/imagecache/’ . $old;
$new_path = $file_url .’/imagecache/’ . $new;
$old_class_size = ‘imagecache-’ . $old . ‘” width=”‘ . $old_w . ‘” height=”‘ . $old_h . ‘”‘;
$new_class_size = ‘imagecache-’ . $new . ‘” width=”‘ . $new_w . ‘” height=”‘ . $new_w . ‘”‘;
$text = str_replace($old_path, $new_path, $text);
$text = str_replace($old_class_size, $new_class_size, $text);
}
}

Search snippets URLs

I’m not sure if the following applies to ordinary Drupal search but it certainly does with Apache Solr Search. The URLs for the individual search results were coming back with the fully qualified URL pointing to the desktop site. This was solved by a bit or mopping up in a base feature, mw_base.

function phptemplate_apachesolr_search_snippets($doc, $snippets) {
// mobile site?
$mobi = module_exists(‘cuf_mobile’) && cuf_mobile_is_mobile();
$url = $doc->url;
if($mobi) {
$url = str_replace(‘http://’, ‘http://m.’, $url);
}
}

GMap

The combination of Location and GMap is a very popular one on Drupal sites. GMap module is currently targetting version 2 of the Google Maps API. Version 3 offers a bunch of new features for mobile devices.

Google Maps JavaScript API V3 “The Google Maps Javascript API lets you embed Google Maps in your own web pages. Version 3 of this API is especially designed to be faster and more applicable to mobile devices, as well as traditional desktop browser applications.”

For now users of GMap are stuck on v2 but there is active development in GMap to bring the module up to support v3.

WYSIWYG

WYSIWYG textareas do not display properly on some mobile devices. You need to turn them off.

WYSIWYG on mobile devices Discussion on WYSIWYG issue queue regarding the difficulties faced on a variety of devices. End conclusion appears to be that you need to turn it off for best results.

How to turn of WYSIWYG? After a bit of poking around I worked out that setting the ‘format’ for the textarea to an empty array was the way to do it. The following code in your mobile module will do the trick for comment and node bodies. If you have other forms which need fixing then you’ll need to do a bit of debugging to suss out what the form_id and element id is.

/**
* Implementation of hook_form_alter.
*/
function mw_mobile_form_alter($form_id, &$form) {
if (!mw_mobile_is_mobile()) { return; }
// turn off wysiwyg forms for textareas. you need to manually find the form
// with wysiwyg and then work out its id and where ‘format’ is.
//print $form_id['#id'] . ‘ ‘;
$no_wysiwyg = array(
‘comment-form’ => ‘comment_filter’,
‘node-form’ => ‘body_field’
);
$id = $form_id['#id'];
if (array_key_exists($id, $no_wysiwyg)) {
//print_r($form_id);
$form_id[$no_wysiwyg[$id]]['format'] = array();
}
}

Tabs

The primary and secondary tabs which appear on the top of the page tend to take up a fair amount of horizontal space and will be the element which causes horizontal scrolling. These tabs can be easily restyled to display as a traditional list. You can also update page.tpl and move the tabs to the bottom of the page so they don’t detract from the main content.

Flash

Flash is not going to work in iPhones, iPad and many other devices. It’s also heavy and resource intensive. As such it shouldn’t reallu be used for content or navigation on the mobile site. The exception might be to show video content, however, even in this case there might be better workarounds.

Suckerfish

Suckerfish provides dropdown menus which can take up a lot of room. The hover metaphor doesn’t work for touch devices. Best avoided.

Make sure that links are big enough to clickable: large with enough whitespace around key navigation links.

YouTube

Mobile devices such as the iPhone and iPad may have a special app to handle YouTube videos natively so clicking on a link is preferable than displaying an embedded video.

Advertising

Yes – ads can be optimised for mobile as well.

Google Mobile Ads “In May 2010 Google acquired AdMob, a leading mobile advertising network that developed innovative mobile-specific ad units and solutions, to significantly enhance our mobile display ad services for publishers.”

Testing

Testing an afterthought? Never :) The fact is that a lot of the hard work is in getting over the config hurdles. once the mobile site is up and running you are going to uncover a bunch of things you never dreamed about. Here’s a quick checklist of things to look out for:

  • UA testing and redirect working.
  • Boost/Drupal Core page caching working.
  • SSL login working OK.
  • Basic functionality and navigation operational.
  • Theming is up to scratch.
  • Site fulfills its goals.
Be Sociable, Share!
Jan 06 2011
Jan 06

K-I-S-S-I-N-G.

I've been very negative about usage of Organic Groups in the past.  ELMS started being built in organic groups in 2007 only to be declared a total failure because of how immature Drupal 5 (and organic groups 5.x) were.  This drove us to use a purely multi-site based approach to site structuring and completely scare me away from infrastructures involving Organic Groups.

Fortunately, a lot has changed since our journey began back in 2007:

  • Drupal and Organic Groups have drastically matured as platforms
  • Community support for Organic Groups via helper modules and distributions built off of it have helped push the notion of OG far beyond just the idea of "that thing that powers groups.drupal.org"
  • Our Unit has matured in our thinking about instructional design as well as how technology can help augment instructional design
  • I'm much more active in the community and we've benefited immensely in terms of bouncing ideas off of colleagues and other project leads
  • The Outline Designer (v 1.2+), backbone of our course development, has support for Organic Groups (6.x)

 All of these things have lead to today's announcement:  ELMS Alpha 2 has been released, running fully off of organic groups.  What does that mean for people that have been interested in ELMS in the past?  The requirement for installing ELMS is stated like this: "Can you install Drupal? good to go!".

Groups in organic groups have been mapped to Courses thanks to some renaming of views, string over-rides module (critical for UX) and a helper module specific to the distribution.

While we still are running a multi-site based stack for our current course infrastructure, we'll be bringing up a modified version of the ELMS distribution on a new machine in the future and chugging ahead, fully in-love with Organic Groups and we think you will too!  Click through to download ELMS Alpa 2 today! Then watch the installation tutorial below!

Dec 15 2010
Dec 15

Yesterday at the Belgian Drupal User Group (DUG) I presented the E-mail Marketing Framework module, also known as EMF. EMF is a Drupal module that makes an abstraction of the APIs of newsletter or e-mail marketing services like MailChilmp, Campaign Monitor, ... Think of it as the Wysiwyg module for e-mail marketing.

E-mail Marketing was developed based on the observation that most of these services like MailChilmp, Campaign Monitor, ... have kind of the same functionality. So EMF focusses on the UI and other functionality and leaves the actual hard work (subscriptions management, ...) to a plugin module. By writing a plugin module for your own mailing service you can immediately enjoy all functionality provided by EMF on your own site.

Still not clear? Watch my presentation:

Oct 29 2010
Oct 29

DrupliconAt a recent St. Louis area Drupal meetup (details here), I presented a quick session on how to build a drupal module, geared towards beginning Drupal developers (I don't consider myself too advanced, but I have found that my experiences can often help others).

I have attached to this post the custom module (a .zip) file that I included for examples in the presentation, and I also uploaded the slideshow (quick and easy - just 12 slides!) to slideshare. I've embedded the slideshow below:

Oct 27 2010
Oct 27

Behold the new and improved Module Finder!

drupal module finder search

Now with color coded direct download links, perfect for copy and pasting into Drupal 7 or your command prompt.

I've also added direct links to each module's project page, issue queue, and usage statistics. Now you can see the module's maintenance status and user count right from within the search results!

Finding and downloading Drupal modules has never been faster or easier. :)

PS: Your donations make it possible for me to keep working on stuff like this. Thanks for your support!

Oct 18 2010
Oct 18

Open Source InitiativeToday I released my first contributed Drupal module, Gallery Archive (backstory here). I had already created the Drupal theme Airy Blue (in use on this site) some time ago, and have created many modules and themes in use on this site and many other Drupal sites I manage. However, it takes a lot more polish, a lot more work, and a lot more long-term dedication to release a module for public consumption!

So, why would I do such a thing? I'm have little time for such projects as it is... and it's not like releasing a module on drupal.org, thus opening up the issue queue for time-consuming support requests is going to make my life any easier.

Well, I have piggybacked on the success and support of tons of other generous Drupal users over the past two years—I have gone from being a complete programming newbie to a competent (but still learning) PHP programmer, and I have gone from learning what 'node' means to understanding much of Drupal's node API (which, of course, is all changing again ;-). I have joined thousands of charitable souls who devote quite a bit of time—personally and professionally—to making great projects like Drupal stronger... without any compensation besides a 'warm fuzzy feeling.'

I feel it's time I 'pay it forward,' as it were. When I read the book "Hackers," I sympathized with the movement of software developers who wanted to simply create new and amazing things for the betterment of humanity (in my case... humanity and the Church).

I am committing myself towards moving my theme and new module forward and releasing Drupal 7 versions of both within the year (sooner or later...), and I feel this is a very good use of my time, both as an Archdiocesan web developer, and an individual in the open source software movement.

I would call anyone else out there who hasn't taken the time to contribute back: please consider doing so—it will help you become a better programmer/designer, and it will help strengthen the community from which you have received so much already.

Average:

Your rating: None Average: 5 (1 vote)

Oct 18 2010
Oct 18
Printer-friendly version

In part 1 of this series of articles about my favorite new Drupal 7 hooks, we looked at the incredibly useful hook_page_alter(). I also stated that in this article I would write about another awesome new pair of hooks:  hook_query_alter() and hook_query_TAG_alter().

If you read my previous article: Drupal 6 vs Drupal 7 Database Primer - Part 1 then you know that in D7 we can build structured queries, also known as dynamic queries. Essentially, we're just creating a query object and by calling certain methods of that query object Drupal generates a SQL statement and executes it. Pretty straightforward, but here's an example as a quick refresher:

<?php
$query = db_select('users', 'u');

$query
  ->condition('u.uid', 0, '<>')
  ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
  ->range(0, 50);

$result = $query->execute();
?>

There's an additional method we can call on this query object:

<?php
$query->addTag('mymodule');
?>

This method "tags" this query object with an arbitrary string, in this case the name of a module (which supports Drupal's loose namespace conventions). Our module or other modules can use the string - or strings as the method can accept any number of "tags" - assigned to this query object to alter that object.

As mentioned, D7 has provided us with two new hooks: hook_query_alter() and hook_query_TAG_alter(). These two hooks take a concept introduced all the way back in Drupal 4.6 which involved using the function db_rewrite_sql() and hook_db_rewrite_sql() to alter queries. This technique, for a variety of reason, was limited to node, taxonomy, and comment queries only. In D7, we can alter any query from any module so long as that query has been "tagged" and we know the tag's name. For that purpose, there are several utility methods we can call to help us:

<?php
// TRUE if this query object has this tag.
$query->hasTag('example');

// TRUE if this query object has every single one of the specified tags.
$query->hasAllTags('example1', 'example2');

// TRUE if this query object has at least one of the specified tags.
$query->hasAnyTag('example1', 'example2');
?>

Let's put all of this together and build a simple Drupal 7 module for demonstration purposes. First, let's outline what we want our module to do:

  1. Define a schema in our module's .install file using hook_schema() to store some arbitrary information that we can attach to nodes.
  2. Implement CRUD functionality for handling our data using hook_node_insert(), hook_node_update(), hook_node_delete(), and hook_node_load().
  3. Restrict or change the data loaded based on some condition using hook_query_alter() or hook_query_TAG_alter().
  4. Display our data when a node is loaded that meets our conditions using hook_node_view().

Ok. A good start. Let's think of a use-case so we know exactly what kind of data we'll be attempting to load with our nodes. For brevity's sake, we will attempt to simply store the IP address and time zone of the node's author when a new node is created. When a node is viewed by a user, we will check that user's permissions and if one condition is met, we will show both the IP address and time zone of that node's author. If another condition is met, however, we will alter our original query and only show the time zone value. Not really all that useful, but this example should illustrate some basic principles that module developers need to know in the field to handle complex business requirements.

Enough talk. Time to code.

First, our module's .info file which should be self-explanatory, but if you need a refresher you can read the official handbook page on D7 module .info files here:

; $Id$
name = Britesparkz
description = "Module for demonstrating various features in D7."
package = Development
core = 7.x
files[] = britesparkz.module
version = "7.x-1.x-dev"

Now for our module's .install file where we will define our schema:

<?php
/**
 * Implements hook_schema().
 */
function britesparkz_schema() {
  $schema['britesparkz'] = array(
    'description' => 'Stores node author IP address and time zone information.',
    'fields' => array(
      'nid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
        'description' => 'The {node}.nid for this authored node.',
      ),
      'uid' => array(
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
        'description' => 'The {node}.uid for this authored node.',
      ),
      'ip_address' => array(
        'type' => 'varchar',
        'length' => 16,
        'not null' => TRUE,
        'default' => '',
        'description' => 'The IP address for this node author.',
      ),
      'timezone' => array(
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
        'description' => 'The time zone for this node author.',      
      ),
    ),
    'indexes' => array(
      'node_author_ip' => array('ip_address'),
      'node_author_timezone' => array('timezone'),
      'uid' => array('uid'),
    ),
    'foreign keys' => array(
      'node' => array(
        'table' => 'node',
        'columns' => array(
          'nid' => 'nid', 
        ),
      ),
    ),  
    'primary key' => array('nid'),
  );

  return $schema;
}
?>

Here is the official handbook page on D7's Schema API for reference. Also, note that in D7 we no longer need a hook_install() to install our schema with drupal_install_schema(). If D7 sees an implementation of hook_schema() in a module's .install file, it will automatically install that schema now.

As you can see, we have defined a table with columns for storing a node's nid, the node author's uid, and the node author's respective IP address and time zone at the time the node was authored or updated. Now that we are all setup, we can get started with the actual module. First, we'll begin with defining a permission for our module using hook_permission():

<?php
/**
 * Implements hook_permission().
 */
function britesparkz_permission() {
  return array(
    'view node britesparkz info' => array(
      'title' => t('View node Britesparkz information'),
      'description' => t('Allows users to view Britesparkz information attached to nodes.'),
    ),
  );
}
?>

A simple implementation of a hook_permission(). We will use this permission to determine if the current user can view both the IP address and time zone. Otherwise, for this example, we will always default to displaying the node author's time zone.

Next, we need to begin work on the CRUD interface required for handling our data. We will use some Node API hooks to facilitate this. First, we need to actually store our data when a node is created or updated. For that we use hook_node_insert() and hook_node_update():

<?php
/**
 * Implements hook_node_insert().
 */
function britesparkz_node_insert($node) {
  $transaction = db_transaction();
  
  try {
    $record = new stdClass();
    $record->nid = $node->nid;
    $record->uid = $node->uid;
    $record->ip_address = ip_address();
    $record->timezone   = drupal_get_user_timezone();  
    
    drupal_write_record('britesparkz', $record);    
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('britesparkz', $e);
    throw $e;
  }
}



/**
 * Implements hook_node_update().
 */
function britesparkz_node_update($node) {
  $transaction = db_transaction();
  
  try {
    $record = new stdClass();
    $record->nid = $node->nid;
    $record->uid = $node->uid;
    $record->ip_address = ip_address();
    $record->timezone   = drupal_get_user_timezone();  

    $result = db_query("SELECT nid FROM {britesparkz} WHERE nid = :nid", array(':nid' => $node->nid));
    
    $exists = $result->fetchObject();

    if ($exists) {
      drupal_write_record('britesparkz', $record, 'nid');  
    }
    else {
      drupal_write_record('britesparkz', $record);  
    }
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('britesparkz', $e);
    throw $e;
  }
}
?>

We begin by telling Drupal that we want to use a transaction with the function db_transaction(). This is a good thing when doing insert and update operations for if anything should fail, the query does not execute at all and we don't have to worry about bad data possibly ending up in our database.

The rest is pretty standard fare. We populate a new object and write that object to our database table. That object's properties correspond to our table's column names so drupal_write_record() does all the dirty work for us.

But what if a node is deleted? Easy. We just delete the corresponding record from our database table using hook_node_delete():

<?php
/**
 * Implements hook_node_delete().
 */
function britesparkz_node_delete($node) {
  db_delete('britesparkz')
    ->condition('nid', $node->nid)
    ->execute();
}
?>

Now we need to actually "attach" our information to a node when that node is loaded by implementing hook_node_load(). That looks like this:

<?php
/**
 * Implements hook_node_load().
 */
function britesparkz_node_load($nodes, $types) {
  $query = db_select('britesparkz', 'b');
  $query
    ->condition('b.nid', array_keys($nodes), 'IN')
    ->fields('b', array('nid', 'timezone'))
    ->addTag('britesparkz');
    
  $result = $query->execute();
   
  foreach ($result as $record) {
    $nodes[$record->nid]->britesparkz = array(
      'ip_address' => isset($record->ip_address) ? $record->ip_address : NULL,
      'timezone' => $record->timezone,
    );
  }
}
?>

Here we use a dynamic query to fetch our information. Note that we only retrieve the nid and timezone here. That's because later we are going to use hook_query_alter() to add in the IP address field if the user has permission to view that. We have also "tagged" our query with the name of our module. We'll use that in our hook_query_alter() to target this query.

Now that our CRUD implementation is done, we need to actually display the attached information when a node is being viewed. For that we implement hook_node_view():

<?php
/**
 * Implements hook_node_view().
 */
function britesparkz_node_view($node, $view_mode, $langcode) {
  if (isset($node->britesparkz) && !empty($node->britesparkz)) {
    $node->content['britesparkz'] = array(
      '#theme' => 'item_list',
      '#title' => t('Britesparkz Info'),
      '#items' => array(        
        $node->britesparkz['timezone'],
      ),
      '#type' => 'ul',
    );
    
    if (isset($node->britesparkz['ip_address'])) {
      array_push($node->content['britesparkz']['#items'], $node->britesparkz['ip_address']);
    }
  }
}
?>

First, we check to see if our data was actually loaded with the node. If it was, we add our information to the node object's content array. This is a renderable array that Drupal parses and turns into markup. For simplicity, I have told Drupal to render our information using theme_item_list(), but I could have just as easily written my own theme function and passed the name of it for the #theme property. Lastly, we check if the IP address for this node has been set, and if so, add it to the item list.

That brings us now to hook_query_alter(). Everything else is in place, but as of yet we will never see the IP address regardless of our permissions. That's because we excluded that field from the tagged query we executed in hook_node_load(). Using hook_query_alter(), we can check the user's permissions and call our query object's addField() method to add the IP address field as such:

<?php
/**
 * Implements hook_query_alter().
 */
function britesparkz_query_alter(QueryAlterableInterface $query) {
  if ($query->hasTag('britesparkz')) {
    if (user_access('view node britesparkz info')) {
      $query->addField('b', 'ip_address');
    }
  }
}
?>

We target our query by calling the query object's hasTag() method and pass it the name of the tag we assigned it. Then we check the currently logged in user's permissions to see if they should be able to view both the IP address and timezone. If they have this permission, we call the query object's addField() method and pass it the IP address column name.

We could have also saved ourselves the trouble of having to call the hasTag() method by implementing hook_query_TAG_alter(). By replacing the "TAG" section of the function name with our tag's name, Drupal will automatically target the appropriate query for us:

<?php
/**
 * Implements hook_query_TAG_alter().
 */
function britesparkz_query_britesparkz_alter(QueryAlterableInterface $query) {
  if (user_access('view node britesparkz info')) {
    $query->addField('b', 'ip_address');
  }
}
?>

Hopefully, this article and the accompanying module help you to see just how powerful hook_query_alter() and hook_query_TAG_alter() can be. There's so many possibilities that I can't wait to see what module developers out in the field come up with using these hooks to bend Drupal to their will.

In the next article in my D7 series, we'll depart from hooks for the time being and look at some of my favorite new Drupal API functions.

Until next time, happy coding

Further reading:

Oct 11 2010
Oct 11
Printer-friendly version

I still remember the first "Aha!" moment I had as a new Drupal developer. I had a node form that needed something or another changed on it, and I remember beating my head against template.php for hours until I finally broke down realized I may need to write a custom module to facilitate this change. At the time (which was like eight years ago), I was very much the Drupal n00b, and my experience with writing custom modules was very limited and relegated mostly to writing custom admin forms and the like. After what seemed like hours of scouring through API documentation and forum posts I saw that what I needed to do was implement something everyone was calling: hook_form_alter().

Eight years later and probably hundreds of thousands of lines of code later, I can't imagine my life as a Drupal developer without hooks. Hooks allow us to, well, "hook" into various parts of Drupal and override, change, or add to its behavior. I won't delve into exactly how hooks work, as that's another article, but suffice to say they make my life a lot simpler.

The move from D5 to D6 did not give us many more new hooks to work, but many existing hooks were improved upon. The advent of $form_state is one major improvement that comes to mind immediately. Well, in D7 hooks have gotten the love they deserve. Yes, there are a lot more hooks now, and yes, that means a larger core, but that also means more flexibility for module developers.
 The first reason for the increase in the number of hooks from D6 to D7 is the separation of hooks with an $op parameter into their own hook implementations. D6 hooks such as hook_nodeapi(), hook_user(), and hook_comment() all took an $op parameter that we could use to operate on node, user, or comment objects at various points in their lifespan. Each $op parameter has now been split out into its own hook. For example, in D6's implementation of hook_nodeapi(), $op had the following possible values:

  • alter
  • delete
  • delete revision
  • insert
  • load
  • prepare
  • prepare translation
  • print
  • rss item
  • search result
  • presave
  • update
  • update index
  • validate
  • view

If you had a module that implemented hook_nodeapi() you probably became accustomed to having to write long switch statements that determined what to do depending on the current value of $op. For instance, this should look familiar:

<?php
function mymodule_nodeapi(&$node, $op) {
  switch ($op) {
    case 'load':
      // do something when a node is loaded
      break;
      
    case 'insert':
    case 'update':
      // do something when a new is created or updated
      break;
      
    case 'delete':
      // do something when a node is deleted
      break
  }
}
?>

While powerful, these hooks could get verbose fast. As stated, in D7, these separate $op values have now been split into their own hooks. So now, we have hooks like: hook_node_load(), hook_node_insert(), hook_node_update(), and hook_node_delete(). This is good as it means less overhead and less code. And I always hated having to write switch statements on $op.

Beyond refactoring some of D6's hooks, D7 gives a load of new hooks. In this article, I want to share a few of the ones that, I believe, are very exciting and will change the Drupal landscape, opening up a new arena of flexibility and features for module developers.

The first new D7 hook I saw that made me do a double-take was hook_page_alter(). Now, in D7, the theme layer has been refactored and pages are rendered from the information contained in an associative array: $page. An example of a $page array's top level elements should make this clearer:

<?php
  $page['page_top'];
  $page['header'];
  $page['sidebar_first'];
  $page['content'];
  $page['sidebar_second'];
  $page['page_bottom'];
?>

As you can see, the top level keys of the $page array correspond to regions. Each of these contain information that Drupal parses through to determine how a page should be rendered. With hook_page_alter(), we can dip into this array and make changes right before the $page array is sent to the theme layer. This is a boon for module developers and theme developers alike. The API documentation has a good example to illustrate what we can do with hook_page_alter():

<?php
function hook_page_alter(&$page) {
  // Add help text to the user login block.
  $page['sidebar_first']['user_login']['help'] = array(
    '#weight' => -10,
    '#markup' => t('To post comments or add new content, you first have to log in.'),
  );
}
?>

Let's say you don't want to display links for a specific node type. Easy:

<?php
  $type = $page['content']['nodes'][$node->nid]['#bundle'];
  
  if ($type == 'article') {
    unset($page['content']['nodes'][$node->nid]['links']);
  }
?>

Get the idea? This is a HUGE win for Drupal, in my opinion, from a productivity perspective. This hook plays very nicely the related hook: hook_page_build(). hook_page_build(), as you might have guessed, allows module's to add elements to the page array BEFORE they are rendered. As as result, hook_page_alter() is called after hook_page_build().

This new power comes to us via Drupal's implementation of "renderable arrays". Basically, anything that will end up as HTML on a page begins its life as a structured array which contains data that informs Drupal of how it should parse it and render the data as HTML. We've seen this before in D6 with the Form API. Now our page content works on a similar principle. So, essentially, if you know how to manipulate an array, you should be able to bend Drupal to your will when it comes to content manipulation. Complex UI/UX-related business requirements should now be fairly straightforward to address in code, which means more time for themers to concentrate on the site's design and usability and not so much about having to write convoluted theme overrides to change seemingly simple page and content elements.

In the next article of this series we'll look at two more brand new Drupal 7 hooks that I'm salivating over trying out: hook_query_alter() and hook_query_TAG_alter().

Until next time, happy coding.

Further reading:

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web