May 29 2015
May 29

Before anything else, I want to point out that while this post will focus on maintaining a contrib module on drupal.org, that is just one of the many ways to contribute to the Drupal project. Every contribution is important whether it's a core patch, a documentation edit, a translation, or something else. If you use Drupal, please consider how you might be able to give back to the community. If you're already contributing, then thank you!

If you're contributing your first module to Drupal.org, the process starts with obtaining GIT access and creating a sandbox project. Once you've done that and you're confident that your module is ready for general use, you must go through a one-time approval process to promote your sandbox project (and any future projects) to full project status. It's difficult to deny that having some sort of manual review process is important but as someone who has been through the process I can attest that it can be frustrating at times.

Several months ago, there was a change to the project application process proposed. The general idea was to let users who haven't finished the application process get an installable release and reserve a namespace while still preventing insecure and poorly coded modules from being published on Drupal.org.

More recently, at DrupalCon Los Angeles, David Hernandez (a member of the Drupal.org Software Working Group) and Michael Hess (Security team lead) gave an excellent presentation about some of the changes that are being implemented as a result of the proposal and discussions between various Drupal.org working groups. Some of the key takeaways from the presentation:

  • Automated code review tools have been integrated into the Drupal.org infrastructure and all projects will be subject to the scanning process with a summary of the results visible to everyone.

  • All projects, even ones created by git vetted users, will have to start with a sandbox project and pass the automated scan before the owner can promote the project.

  • Non-vetted users (users who have not had a module go through the manual review process) will be able to promote one sandbox project to a full project but it will only be able to have development snapshots (no beta, 1.x, etc releases) and the project page will indicate that it is maintained by a non-vetted user and not subject to security advisories

  • The manual reviews will no longer be as strict as they were in the past. Instead of flagging every minor issue, the reviewers will focus on security, whether everything is properly licensed, and whether it's clear that the maintainer understands the Drupal API (this is already being done by many reviewers but will now become an official policy).

Overall, anything that might help increase participation in the Drupal community is a good thing. The various Drupal.org working groups have done an excellent job balancing that with the need to maintain quality and security and I look forward to seeing the changes in action once they have been fully implemented.

If you're interested in finding out more about the future of of the Drupal.org project application process, check out the video of presentation below.

[embedded content]

May 14 2015
May 14

Trellon presented a session about Agile design techniques at Drupalcon Los Angeles which was very well received. Over 100 people were in attendance to hear Blake Maples, Mike Haggerty and Jake Tooman talk about the way we introduced Agile design within Trellon and made it work for the groups we serve.

Slides from the presentation are available at the following URL:

https://docs.google.com/presentation/d/1XwXELD7pIQ8CQUuNCv00hxaavUWeaKBj...

... and, if you attended, you can rate the session at the following link:

https://events.drupal.org/losangeles2015/sessions/agile-design-and-plann...

And here's the video! It was up within an hour of session, great job Drupal Association!

[embedded content]
Aug 13 2014
Aug 13

The Drupal community is hard at work delivering the next major release, Drupal 8. If you are already involved, your help is much appreciated. If not, but you would like to help with Drupal core development and are looking for a way to start, take a look at core mentoring hours. It's a great way for people to get involved, and there are several time slots each week that suit many people's schedules.

The list of new features in D8 is impressive and one of the major improvements is the Configuration Management Initiative (CMI), also sometimes called 'better features in core'. This is an effort to make it easier to export the configuration of your site through a UI using the features module.

So, what is the problem features module addresses? And why we do we need a “better” way to export configuration through features?

If your site is something bigger than a personal homepage, you probably have several copies of your site. Let's call them environments. We usually refer to environments by name, such as development, production, staging, etc.

Drupal is known for its flexibility when it comes to configuring different aspects of web sites. The problem that features solves is making your configuration portable. For instance, when a site builder creates a beautiful image gallery and wants to move it from his development server to a production server, can it be done in a way where we know that it's going to work on the new site?

Here's where the features module helps us. Instead of writing down all manipulations made in admin UI to achieve the final result (brilliant-looking image gallery, remember?), and manually repeating them on production server, we create a “feature” for that image gallery and move it to the production environment.

The idea is simple yet powerful – group a set of site settings around some functionality (image gallery, blog, whatever), and save them within the file system. The Features module has a wide range of supported settings out-of-the-box, and there's a robust API to allow other modules to declare “feature-able” settings. Every feature is exported to a Drupal module, so you can copy it to production server, enable it, and get exactly the same settings you defined during development.

It's a very nice tool to have, and it makes it much simpler to control how sites are configured. But what if you want to change the settings that are part of a feature? Maybe you need to reuse that feature across a range of websites, and you need to know that it's going to work even in environments that are different than the production environment you originally built it for.

It's possible to do this right now. The challenge is that, once you change some part of a feature, the feature itself is now overridden. When it comes time to update the feature, or install a new version, you are in a situation where your changes can be overwritten. You can “play” with the settings that are part of a feature, and then return to default state (revert feature), or save the new state as a new default.

There are caveats, of course. Otherwise, who would need “better” features?

First of all, it's a common situation that certain settings are always different on development and production. i.e. we build a page layout using panels or display suite, but want to always display some visual indication for development site (different logo / slogan / bold text 'dev' all over the place?) There is no simple way to do that using features.

Second, and most common, it is not always easy (or possible) to revert feature to default state. When it happens, feature is stuck in Overridden state, and you can do nothing about that.

I'm going to describe features usage in more detail in the next article, and talk about what makes for a better features set. Stay tuned!

Aug 12 2014
Aug 12

If you are doing a lot of theming in Drupal with Sass and Compass, there's a good chance your stylesheets rely on specific versions of gems to compile properly. Mixins and functions can change, and sometimes gems rely on specific versions of other gems to work properly.

Thankfully, there's an easy solution for managing your ruby gems called Bundler. Bundler allows you to manage the versions of gems that are being used for a specific project. With it, you have a way to manage the versions of gems installed in Ruby without needing to switch Ruby versions for each project. It's a great tool for ensuring teams of people are all working in the same environment, and that you can maintain your own Sass projects over time.

This article explains how to get started with Bundler, what a Gemfile looks like, and how you can incorporate it into your own projects.

Installing Bundler

I'm going to assume you already installed ruby and rubygems (which is built into ruby since 1.9). If not, there are a ton of great tutorials available for every platform out there.

Installing Bundler is easy The first step is simply to tell rubygems to get a copy of bundler and install it on our system:

$ gem install bundler

This can be done right through the command line. There's no need to download any packages, ruby knows right where to get the latest version.

Creating a Gemfile

Next, we're going to create a file in your project called a Gemfile. The Gemfile specifies what gem versions are required by your project. This applies to Sass and Compass, along with whatever other gems you might be using. For Drupal themes, the Gemfile should live in the root of your theme (i.e. sites/all/themes/my-theme/Gemfile).

Bundler will create the gemfile for you through a simple command. Go to the root directory of your project and run the following.

$ bundle init

Bundler will create a Gemfile for you, using whatever gems it currently finds on your system. For most themes, this would mean it's using whatever versions of Sass and Compass you have installed on your system. Once the file is created, you can specify specific versions of other gems to use with your project.

What's in a Gemfile

A Gemfile contains information about the gems that are being used for your project. It includes the name of the gem, and information about which version to use. It's very similar to a Drupal install profile, in concept and syntax. If you create a Gemfile and open it, you are going to see something similar to the following.

gem 'compass', '~>0.12.2'
gem 'sass', '3.2.8'

In this example, we're saying we need Compass version 0.12.2 or higher, and Sass version 3.2.8. You will notice you can specify a specific version for any project, or a range of versions that are applied. From here, you can add whatever other gems are needed for your project.

Installing Gems

This is where Bundler does it's work. When you have a Gemfile, you can tell Bundler to set up all the gems needed for your project for you. It will read the Gemfile and ensure that all the required gems are available, with the right versions for your project. You can install gems by the following command in the same path as your Gemfile.

$ bundle install

Bundler will install the correct versions of the Gem on your system and they will be available when it comes time to work on your project. This will not change the default versions of gems that exist on your system. This ensures you are able to work with gems without needing to make wholesale changes to your environment.

Compiling CSS with Bundler

When it comes time to work with your project, instead of using compass watch, you will compile your code through Bundler. This makes sure you are using the right versions of gems as part of your project. The command to compile a project using Bundler is the following.

$ bundle exec compass watch

That's it! Bundler will read the contents of the Gemfile and only use the versions you want. Bundler isn't only for compass though; you can use it to control the version of any gem being executed.

Pro tip: It's helpful to leave a comment in your Gemfile telling other developers how to use bundler. You can do this using a # symbol at the start of any line.

There's more information about how to write Gemfiles and specify gem versions available at the Bundler website. Bundler has been a huge time saver for here at Trellon. I hope this advice saves you the frustration of dealing with multiple versions of ruby gems.

Aug 12 2014
Aug 12

Looking to write CSS for your Drupal sites faster and easier, and eliminate common defects? The first step probably involves choosing a CSS preprocessor to make your stylesheets programmable and accelerate the production of code. There are several out there which are widely supported, but the ones we really like to use are SASS and Compass. This article explains how to set up these tools for use within Drupal.

SASS is a CSS preprocessor. With it, you can write programmable stylesheets - meaning that variables, nested code, mixins, and inheritance all become part of how they are written. These stylesheets then compile into traditional CSS, saving time and effort by keeping the code small and the generated output compatible with all major browsers.

Compass adds another layer of efficiency to the process by providing support for vendor-specific markup and common design patterns. Instead of having to remember to prefix border-radius properties with all the current vendor-specific mixins, you can write a single line of code that will compile with support for them automatically built in.

Together, these technologies work to reduce the complexity of your themes. Instead of creating style rules that apply to individual page elements, you can create rules that are inherited by multiple styles and used throughout your CSS code. You can specify variables that are used throughout your theme (for instance, colors) and know that they are being used correctly every time they are applied. This eliminates some of the more common deficiencies with themes and improves your ability to support your theme over time.

Several popular Drupal themes support SASS and Compass natively (most notably, Zen) while others can have support added without too much effort. It's easy to get started using them, and here's a guide for how to get going:

Install SASS

SASS and Compass are widely supported on all major operating systems. You can work with them using one of the major applications for compiling SASS files. These applications are great for designers who have no interest in the command line and are looking to get started right away.

For people who want to use the command line, however, the first step in setting up SASS is installing Ruby.

What what what? Ruby is not Drupal! Well, websites built in Ruby are certainly not Drupal, but Ruby is a lot more than a framework for developing web applications. SASS was originally written in Ruby and uses it to watch for and compile changes to style sheets.

Linux

If you are using Linux, start by installing Ruby via apt, rbenv, or rvm. Depending on your distro, it may already be installed.

OSX

If you are using a Mac, Ruby comes pre-installed. You may still want to download RVM, however, to allow you to switch between different versions of Ruby.

Windows

If you are using Windows, the best way to get started is using the Ruby installer. It will set up Ruby on your machine so that it can be executed on a command line.

Installing SASS

Once Ruby is installed, installing SASS is easy. From your command line, enter the following:

gem install sass

This will install SASS on your machine. If you run into errors, it probably means you need to run this command with sudo.

Install Compass

With Ruby and SASS installed, it's much easier to install Compass. Enter the following in your command line to set up your system to use Compass.

gem update --system
gem install compass

Create a Project (Optional)

If your theme is already configured to use SASS, you will find a file called config.rb. If that's there, great, you are ready to get started working with SASS and Compass right away.

If not, you will need to create a new project. Creating a project means you are creating a set of files SASS will use to compile your stylesheets. You can create a new project using any one of the apps mentioned, or by running the following through the command line:

compass create [project-name]

Just replace [project-name] with the name of your project, and Compass will create the following files and directories.

  • config.rb - which houses the settings used when Compass compiles your stylesheets
  • sass/ - which houses the SASS files that will be used to create stylesheets
  • stylesheets/ - which houses the stylesheets that will be generated by SASS

A few things to keep in mind:

  1. Drupal themes commonly use a css/ directory to house stylesheets. If this is important to you, you can specify the directory where stylesheets will generate by adding --css-dir "css" to the command above.
  2. In the sass subdirectory, several files will be created: screen.scss, print.scss and ie.scss. You can use these to write your SASS code, but you don't have to. Any files you add to the sass directory will be automatically compiled for you.

Watch Your Project for Changes

Once you have SASS and Compass set up and have created a new project, the next step is to tell Compass to watch for changes. Every time a file changes in your theme's SASS directory, Compass will compile a new version of the stylesheet. To tell Compass to watch for changes, you can enter the following on the command line.

compass watch [path-to-project]

If you watch your command line, it will provide an ongoing list of files that have been compiled and errors that may appear.

And that's it. You are now ready to get started with SASS and Compass. Happy styling!

Aug 08 2014
Aug 08

Drupal 8 is not far off from being released, and you may have heard some chatter about the differences in how you create custom modules. The reality is that, while there are some differences, it's not really that hard to wrap your head around them. This article provides a gentle introduction to creating forms in Drupal 8, and highlights the differences and similarities to how you would do this in previous versions of the platform.

Step 1: Create the Info File

Let’s start by creating a new custom module called Amazing Forms. Under modules, let's create a folder called amazing_forms, and within it let's create a yml file. A yml file (or YAML, which stands for "YAML Ain't Markup Language") is a file that specifies the configuration for a file.

Let's create a yml file called amazing_forms.info.yml. For all practical purposes, it's very similar to the .info files we used in Drupal 7. Let's put some basic information into the file, to describe the module and tell Drupal what it is supposed to do.

amazing_forms.info.yml:

name: Amazing forms
type: module
description: 'Demo for creating Drupal 8 forms.'
package: Custom
core: 8.x
hidden: false

The basic settings we are providing in this file should be recognizable to anyone who has created a custom module in Drupal 7. While there are a lot of other settings that could go into a yml file, we are really only concerned with the basics for this demonstration.

Step 2: Create the Files for the Module and the Controller

This step is where things start to get a little more interesting. In Drupal 7, we would simply create a module file and start coding. In Drupal 8, we do things a little differently.

Start off by creating a file called amazing_forms.module, and leave it empty for now. This is the module file we all know and love, and it's where most of our code will live.

The next thing we are going to do is create our forms. Instead of defining the forms in the custom module, we are going to create a custom controller to handle how the form works in our site. While the word controller may sound unfamiliar, it's not something to be afraid of. In PHP, controllers are really just elaborate functions with some special properties that make them easier to work with. When you use a controller class, you are technically creating an object, which has it's own functions (called methods) attached that you can use to manipulate through code.

To create our controller, we are going to create some subdirectories to store the code in a way Drupal can recognize. Create a directory called src, and within there, create another directory called Form (and mind the capitalization). This is where our controllers are going to live, and it's where we will be doing most of the work to create this module.

Step 3: Create the Controller

So, finally, we get to write some code. Within the src/Forms directory, create a new file called ContributeForm.php. This file is going to store the controller for our form. The controller is going to include some unique properties, as well as some methods that are very familiar to experienced Drupalists. Here's the basic skeleton for our controller.

<?php
/**
 * @file
 * Contains \Drupal\amazing_forms\Form\ContributeForm.
 */
namespace Drupal\amazing_forms\Form;
use
Drupal\Core\Form\FormBase;
use
Drupal\Core\Form\FormStateInterface;
use
Drupal\Component\Utility\UrlHelper;/**
 * Contribute form.
 */
class ContributeForm extends FormBase {
 
/**
   * {@inheritdoc}
   */
 
public function getFormId() {
  }
/**
   * {@inheritdoc}
   */
 
public function buildForm(array $form, FormStateInterface $form_state) {
  }
/**
   * {@inheritdoc}
   */
 
public function validateForm(array &$form, FormStateInterface $form_state) {
  }
/**
   * {@inheritdoc}
   */
 
public function submitForm(array &$form, FormStateInterface $form_state) {
  }
}
?>

So, you will notice our controller is going to include a namespace, which is a unique name assigned to our module. Namespaces have a lot of uses, but the basic one everyone needs to understand is that they ensure code works together within a website. Next, you are going to notice some use statements. These statements are there to tell Drupal to include some other controllers before this one is called, to ensure they are available inside our controller.

Any time you are creating a form in your site, you can write a use statement for Drupal\Core\Form\FormBase. Any time you are creating a configuration form (aka an admin form) you also need to include a use statement for Drupal\Core\Form\ConfigFormBase.

While these conventions may seem new, there's a few familiar things in there as well. Our controller also includes 4 methods. Look at the names - getFormId, buildForm, validateForm and submitForm. These methods are going to be familiar to anyone who already knows Drupal's Form API, they are very similar to the handlers we used to develop around forms in Drupal 7. Even the variables are similar (array &$form, FormStateInterface $form_state).

Let's look at how each of these methods can work. getFormId is where we assign a unique id to the form being created, and it might look like this:

<?php
 
public function getFormId() {
    return
'amazing_forms_contribute_form';
  }
?>

buildForm would be used to actually create the form, and it could be filled out like this:

<?php
 
public function buildForm(array $form, FormStateInterface $form_state) {
   
$form['title'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Title'),
     
'#required' => TRUE,
    );
   
$form['video'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Youtube video'),
    );
   
$form['video'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Youtube video'),
    );
   
$form['develop'] = array(
     
'#type' => 'checkbox',
     
'#title' => t('I would like to be involved in developing this material'),
    );
   
$form['description'] = array(
     
'#type' => 'textarea',
     
'#title' => t('Description'),
    );
   
$form['submit'] = array(
     
'#type' => 'submit',
     
'#value' => t('Submit'),
    );
    return
$form;
  }
?>

validateForm would be used to check the values in the form before they are saved and throw errors, similar to how you see here:

<?php
 
public function validateForm(array &$form, FormStateInterface $form_state) {
   
// Validate video URL.
   
if (!UrlHelper::isValid($form_state->getValue('video'), TRUE)) {
     
$form_state->setErrorByName('video', $this->t("The video url '%url' is invalid.", array('%url' => $form_state->getValue('video'))));
    }
  }
?>

And, finally, submitForm would be used to actually carry out the form submission process:

<?php
 
public function submitForm(array &$form, FormStateInterface $form_state) {
   
// Display result.
   
foreach ($form_state->getValues() as $key => $value) {
     
drupal_set_message($key . ': ' . $value);
    }
  }
?>

So, as you can see, the process for creating a form is actually conceptually similar to what we have been doing in Drupal with the Form API in previous versions. Controllers are really just a way of packaging the hooks we would have used in a form where they are more readable and easier to work with. Instead of writing a bunch of functions with names like amazing_forms_contribution_form_submit, we are using shorter, more declarative names that exist in their own namespace.

Step 4: Add a Router and Call it a Day

Now that you have added a controller to your custom module, you are almost done. The last thing to do is create a router file.

A router file is something that tells Drupal how to find controllers in your site. It's another yml file, and it contains information about the path and location of files. You can use these files to specify permissions and variables to be passed to the theming engine. Create a file called amazing_forms.routing.yml in the amazing_forms module directory, and include the following content:

amazing_forms.routing.yml

amazing_forms_contribute:
  path: 'amazing-forms/contribute'
  defaults:
    _form: '\Drupal\amazing_forms\Form\ContributeForm'
    _title: 'Conribute page'
  requirements:
    _permission: 'access content'

This code defines the path you use to access the form through a browser, the page title, the path for your controller and the necessary permissions for accessing the form.

Final thoughts

Creating custom modules in Drupal 8 may seem hard, but there's more there that's similar than what's new. Yes, object oriented programming is a complex subject, and this article only skims the surface. Practically, in most situations, a basic understanding of controllers, paths and yml file structures will allow you to get started working with the newest version of the platform. In exchange, you gain more power and flexibility around the architecture of your sites.

Sep 10 2013
Sep 10

Last week, we rolled out the first major feature for CRM Core, called CRM Core Donation. It's available for download now from Drupal.org. You can also download an install profile with CRM Core Donation pre-configured, along with sample forms and data to play around with. This is exciting news. The promise of CRM Core has always been to have small, useful applications that extend it's basic functionality in interesting ways. Executing on that vision has taken a lot of work, but it's yielding some great results in terms of new ways to work with information about people in Drupal. We now have a video explaining the basics of setting up online donation functionality in your Drupal website.

[embedded content]

This video is part of a series, and demonstrates how to set up online donation forms with CRM Core Donation and use them within donation pages to interact with contacts. Here's a run down of the specific topics covered: 0:42 - where to download CRM Core Donation 1:31 - discussion about the relationship between forms and donation pages 3:07 - creating a new online donation form 4:10 - configuring a donation activity record in a profile form 5:04 - adding commerce items to a profile form 6:40 - adding default values to a profile form 7:59 - token support in profile forms 8:45 - configuring basic settings for a profile form 10:00 - configuring integration between a profile form and Drupal Commerce 11:16 - configuring amount fields in a profile form 12:07 - configuring payment processors in a profile form 12:30 - configuring name and billing address information in a profile form 14:11 - building a new donation page, associating it with a profile form 16:00 - demonstration of a fully configured donation page 18:02 - explanation of a donation record (used to track donations after they have been submitted)

What's Next

CRM Core Donation is designed to act as a fundraising management system. We are shooting some additional videos that go into the details of how to make it do much more than simply generate donation pages. The next video will cover administering CRM Core Donation, covering topics like setting up personalized HTML emails, working with multiple payment gateways, recording offline donations, source tracking, duplicate matching and turning donors into users. After that, the next release is a video explaining the reports in CRM Core Donation, how to customize them to the specific needs of your organization, and how to author your own reports about donation activity.

May 08 2013
May 08

Today, Trellon released new versions of CRM Core and CRM Core Profile. Both of these modules include new features that are important for anyone looking to build modules that use CRM Core as a backend for storing contact information.

These releases are part of our Garden Party roadmap, as part of the 'Live Music' stage. They are for site builders and developers looking to build modules and features that expand upon CRM Core's basic capabilities. With them, you have some more powerful tools for working with contact records stored in your Drupal site, and they include some usability enhancements based on feedback we received as part of the 0.91 release of CRM Core.

I wanted to share a little about what you will find, and why it is important.

CRM Core - The New Stuff

CRM Core 0.93 is the second stable release of CRM Core. We received a lot of feedback from the community around the 0.91 release and tried to incorporate as much of it as possible.

This new release really gets into the issue of flexibility in CRM Core, in terms of how it can be used to support a wide variety of situations where you need to manage contact records on a Drupal website. As part of the 'force is with you' philosophy behind CRM Core, many of the new features are built in a way where people can customize the fundamental way the system works without too much effort. We are thinking about site builders and developers with this one, and how to make CRM functionality work exactly the way you want.

Usability Enhancements

There are a lot of usability enhancements that went into this release. Some of the major things you will notice right off the bat:

  • You can now create activities and relationships without first selecting a contact.
  • You can now declare primary contact fields for different contact types. This means you don't need to know the fields someone has configured for contacts in order to get in touch with them. It is meant to make it easier for module developers to work with data fields that are never static.
  • There are more action links, better breadcrumb support, better page titles, etc. It's just easier to understand what you can do with the system.

Feature Additions

This release includes several major features enhancements. You can find documentation for for each one in the project handbook on Drupal.org.

  • CRM Core Match has been added as a tool for identifying duplicate contacts. On it's own, it does very little, it simply allows administrators to control the logical rules for how duplicates are identified through the use of matching engines. CRM Core Match controls the order in which they are processed and hands off duplicates for other modules to work with.
  • CRM Core Default Matching Engine was added. This module creates a matching engine for CRM Core Match. It includes an interface for administrators to configure the logical rules for identifying duplicates at the field level, and operates on a per-contact type basis.
  • The CRM Core Contact object was modified, and now includes a ->match method. This means, any time you load a contact, you can also load a set of duplicates. Very useful for building interfaces that allow people to find matches.

CRM Core Profile - It's a Form Builder

CRM Core Profile has gone undergone some major rework to make it more useful. What we now have is a system that can create forms for working with contact information in various situations. It is contextually aware, meaning you can create forms that preload contact information, automatically identify duplicate contacts, or just allow bulk entry without needing to do any checking.

Rather than go down the list every of feature (there are a lot of them), I am going to talk about the different screens you will find in the module.

The Profiles Screen

CRM Core Profile provides a screen that lists all the profiles configured for your website. This screen shows you the name, machine name, and some other information about each profile. It tells you whether there's a page view of the profile available, and the path for how to access it. It also includes links for editing the profile, changing the settings, cloning, exporting and deleting. All the things you would expect for a good entity!

The Edit Screen

This is where you build your form. CRM Core Profile allows you to create forms that can store contacts and activities within CRM Core. Since there can be many kinds of contacts, and many kinds of activities, we created a form builder that allows you to build the form that represents the information you are trying to capture.

With this screen, you can select the different types of contacts that are included in your CRM Core website, along with different types of activities. You can select the specific form fields to capture and configure them to work the way you wanted. If you are looking to just capture information from users, great, that's easy to do. If you are looking to hide certain fields, prepopulate values, etc, it's easy to do that as well.

The Settings Screen

Profiles are meant to capture information. The settings screen is what lets you tell CRM Core what to do with that information.

With this screen, you can select options in a number of areas. CRM Core Profile allows to to present a CRM Core Profile as a page, set submission messages, set paths to the page, and tell forms where to redirect through a simple interface. It also allows you to control how the form is prepopulated, using information from the currently logged in user or URL tokens. It also provides some controls for how to match contacts, allowing you to turn off contact matching altogether if you are looking just for a bulk entry form.

I did not want this post to get too long. Expect a video in the next few days explaining how this all fits together.

May 02 2013
May 02

For those of you attending Drupalcon this month - you have good taste! Show exactly how good it is by joining us for a special event to benefit the trust of Aaron Winborn, a long time Drupal contributor who has been fighting ALS for some time now.

This event is on May 20, 2013, it starts at 4:30 and will be held at the offices of EcoTrust. Their address is as follows:

721 Northwest 9th Avenue
Portland, OR 97208

To attend, you can register here:

http://pdxdrupaldogooders.eventbrite.com/

If you would like to make donations to Aaron's trust, it would go a long way to helping him and his family deal with the costs of battling the disease. Even if you can't attend, you can contribute by going here:

http://aaronwinborn.com/blogs/aaron/special-needs-trust

Looking forward to seeing you in Portland!

Mar 28 2013
Mar 28

How do you build a feature that is going to work on any Drupal website? The question is more complex than it seems, and there is an important discussion going on around it in the Drupal community. It's playing out in a number of different areas where smaller groups of developers seek to leverage features as a tool for sharing functionality, and this issue is going to have an impact on the way people think about building websites over time.

For those of you who work with features regularly, this conversation is about building small, useful applications that can be added to any Drupal website to extend its functionality. It goes beyond packaging up a bunch of presets to move code between test and production environments, or bundling a set of features that are going to work together as part of a single distribution. The common idea people seem to be wrestling with is how to build features that will reliably work together regardless of where they are deployed, and ensuring they will not conflict with others people might be building.

Nejo Rogers from Chocolate Lily wrote a great article about code portability back in July 2012 that summarizes some of the different approaches to this problem, and the challenges that come along with each one. Basically, the standard approaches to the problem are apps, distributions and writing standards around compliance. You can read about the benefits and limitations to each approach over there. Suffice to say, there is no solution that completely addresses the issue of portability in a manner that is free from conflict.

Part of the reason this is such a tough challenge to deal with has to do with the standard ways we think about application development. When bundling a feature, you get into questions about how data is stored, how the interfaces will work, and business logic associated with the code. If you are doing some really fancy with custom entity types or integration with external services, the feature itself gets a lot more complex. No one really builds features the way they build modules - the standard practice is to bundle content types (along with their fields), various displays, and other elements together into a single package that is then ready for distribution.

There are some the ways this can break down, which might not be obvious when someone is packaging a feature. These are the kinds of questions various groups are trying to tackle. Let's say we are building a feature around timekeeping, it has its own content types, and it ships with various interfaces built around contexts and panels. You can run into issues that break the portability chain in situations like this:

  • The feature is useful, but the interfaces aren't going to work in my use-case. How can I just get the entities?
  • I have this other content type called timekeeping, and it does something entirely different. How do I reconcile the very similar content types?
  • I have a bunch of fields that share names with the ones used in timekeeping content types. How do I keep the conflict from affecting my site?
  • The feature requires some fields to be configured on other content types. How can I build portable features if I don't know if the other fields will be there?

When any of these issues affect a site, portability is lost and suddenly developers are dealing with questions of integration and customization. Some of them can be easy to deal with, others not so much. They all have the potential to create situations where a feature is only as useful as the day it was installed, as customizations can make it very hard (if not impossible) to upgrade a feature over time.

Groups are starting to spring up, where developers are looking to build communities of practice for building Drupal sites using features based approaches. They are concerned with using features as a tool for more efficient web application development, where questions around portability of code are key to their success. Many of these groups are focused on niche use cases that may not affect the way everyone wants to work with Drupal, but the way they are addressing the issue is important to pay attention to.

For example, this is happening at Stanford University, where Drupal is becoming a standard platform for web application development. Ease of use and code portability concerns are important there for reasons going beyond supporting a single cluster of sites, they affect how people perceive the viability of the platform. Some of the people working on the problem aspire to build communities that go beyond the boundaries of a single institution. Check out the Drupalcon session that talks about his vision for building an education consortium around the use of portable features.

I happen to know Zach and am familiar with his approach for dealing with the question of code portability. It's a standards-based approach that draws from the Kit standards for feature development and extends it in some important ways. While his efforts in this area are just ramping up, it's going to be useful to watch the way this comes about. His group's efforts are important as an example of breaking down institutional boundaries and leveraging the work of a larger community in an area that could really benefit from some innovation.

Trellon deals with this as well. We recently released the first stable version of CRM Core, which incorporates some good approaches to portability and feature sharing within Drupal websites. The next step for us is to build a community that shares features around the module, and we have run into all kinds of questions about best practices for how to deal with issues around portability. There are some best practices listed over on Drupal.org that haven't reached the point of becoming standards, but are on their way to getting there. In this case, we are dealing with thoughtful ways for how to work around a set of common entities that can be deployed in a lot of different ways. Some of the solutions are obvious, others are really counter-intuitive and not what you would consider when architecting a solution unless you really took the time to think through the issue.

It seems like the Drupal community is on the road towards dealing effectively with the question of portability in features, there is a lot of promise as well as questions for how things will be worked out. What is important to recognize in the cases of Stanford and CRM Core is that these approaches are built around standards for how a community would share features. The way I think about standards is they are an operator solution to a problem that has yet to be tackled by technological means. The work of each of these communities deals with them is instructive in terms of the kinds of challenges the greater Drupal community has yet to tackle.

Mar 23 2013
Mar 23

The documentation for CRM Core has been updated on Drupal.org. Now that the module has achieved a stable version, we have updated most of the information available about how to use it.

Go check it out.

The CRM Core handbook contains information on the following topics:

  • A getting started guide, meant to take people through the interface and acquaint them with all the major screens and features.
  • An administrative guide, explaining how to configure the system to work in your Drupal site.
  • A developers guide, which continues to grow, which explains the UI, provides some guidelines on how to build things with CRM Core, and some practical examples for how to work with it.
  • An FAQ, answering all the questions we keep getting asked about the module.

This documentation has been updated with site builders in mind, and written at a level where just about anyone can understand what it says. Take a look at it, and let us know how we could improve it more in the comments.

Mar 22 2013
Mar 22

Today marks the first official, stable release for CRM Core.

Many thanks to all the people who helped test and shared ideas to bring together a stable release, this would not have been possible without all the support and feedback from the community.

Our team is really proud of the work that has gone into CRM Core, and we look at this release as a big step towards introducing native contact relationship management features to Drupal websites. CRM Core allows site builders to build strong, reliable systems for managing contacts, activities and relationships within Drupal. It is built to integrate with major components in Drupal websites, allowing developers to work with familiar controls and techniques for building out advanced systems for creating relationships.

You can check out a copy of the module on drupal.org.

Over the next few months, additional features will be introduced that expand the capabilities already in the system. More specifics about the new features will become available the closer we get to Drupalcon Portland, where Trellon plans to unveil a very interesting installation profile built on CRM Core that showcases what it can do.

The new release is tagged 7.x-0.91. To explain the versioning: 0.9 is the first stable release. Major improvements have been made to the UI, the business logic, and the out-of-the-box experience for new users running existing Drupal websites. There are some other features included in development releases of the module that need more testing before they can be considered stable. These components are considered essential components for a feature-complete module, and will be added back in as part of the 7.1 release.

You can read all about it in the release notes, which follow at the end of this message.

Some other things you might want to be aware of:

  • The documentation for CRM Core is undergoing a major upgrade. The focus of the documentation is on the setup and administration of a Drupal website using CRM Core. It should be useful for anyone looking to test the out of the box experience, and answers a lot of questions that have been coming up in the issue queue.
  • The development branch does contain a number of interesting things we are actively working on. If you are interested in case management, uniform rules for contact matching, or organic groups integration, you might want to take a look there.
  • The issue queue is being cleaned out. The developers are being pretty aggressive in closing tickets, since the stable release addresses a number of functional and conceptual issues that were hard to diagnose with development releases. If there is some big issue you have been waiting on, you might want to check the official release first to see if it is still an issue rather than waiting for an update to a ticket.

Here's a copy of the release notes, which explain what to look for in 0.9 releases.

This release includes a number of improvements over development releases that address issues with the default user experience, business logic, appearance of UI elements, and code stability. Several modules contained in development releases have been removed from this release, and can still be accessed within the development branch.

New features

  • Addition of settings screen, with options for controlling the theme, primary links, and contact info for linked user accounts.
  • Addition of user matching screen, for linking user accounts to contacts.
  • Addition of inactive relationships screen.
  • Addition of navigational menu for CRM Core, and creation of CRM Core Admin menu.
  • Addition of CRM Core Report.
  • Implementation of theming functions and code hinting for all CRM Core entities.

Removed from this release:

The following modules, which exist in previous development releases, were removed from this release:

  • CRM OG Sync
  • CRM Core Case
  • CRM Core Match
  • CRM Core Default Matching Engine

These modules still exist in development versions, they are just not considered stable.

These modules will be supported in future official releases of CRM Core. You can find them the --development branch.

Other commits:

  • Implemented consistent path schemes for all CRM Core entities.
  • Implemented consistent breadcrumb structure for all CRM Core entities.
  • Removed the default image field for contacts.
  • Removed default contact fields from the user edit form.
  • Corrected bugs with bi-directional relationships.
  • Modified appearance of various screens for adding CRM Core entities to ensure there is a uniform appearance.
  • Modified various page titles for compliance with Drupal standards.
  • Modified no results text on contacts, activities and relationship pages.
  • Added action links for adding CRM Core entities.
  • Added operations columns to screens for displaying CRM Core entities.
  • Modified links for adding new contacts.
  • Added reset buttons and other UI controls for various filters throughout the system.
  • Improved UI for user synchronization.
  • Enhanced user synchronization options - system now allows for weighted rules matching.
  • Enhanced user synchronization options - system now allows for batch processing of user accounts.
  • Enhanced sync form to only display users / contacts with no match.
  • Improved dashboard handling - removed default text.
  • Improved dashboard handling - added UI for configuring dashboard.
  • Improved dashboard handling - added controls to remove links to dashboard when it is not present in the system.
  • Removed unnecessary CSS throughout system.
  • Adjusted paths for adding entities within the dashboard.
  • Business logic - CRM Core now allows contacts to be removed from activities.
  • Business logic - CRM Core now allows multiple contacts to be added to activities.
  • Business logic - UI now displays information about contacts when viewing an activity.
  • Business logic - UI now displays participant names as links to contact records.
  • Business logic - streamlined process for deleting relationships.
  • Dependencies - removed unnecessary dependencies.
  • Template files - removed duplicate template files, added theme hinting.
Nov 19 2012
Nov 19

Last week was the third weekly sprint for the CRM Core Demo.

What did we accomplish in this past week?

  • The repository was moved from trellon to github to facilitate community involvement.
  • We created petition list for the online petition feature.
  • Contact petitions /crm/contact/%/activity/petitions which shows the Contact petitions.
  • We created a new field which will store Tags and started Petitions Primer view.
  • A project lead reviewed the online petition feature and news feature.
  • We started CRM Core Views Bulk Operations integration.
  • We resolved conflicts from converting crm core profile to git submodule.
  • We Updated to latest crm core for admin/crm menu item.
  • Bug Fixes
  • We worked on documentation

How you can get involved

In addition to sprinting, we've had quite a few people sign up for additional information and/or to sprint themselves. Sign up if you want to help out, or if you just want additional information. Let us know what interests you have - where you'd like to pitch in whether it is building a feature that you think might be useful, helping with documentation, coding, or theming. If you've already signed up, you should expect an email from the team in the next day.

If you want to hang out with the CRM Core sprinters, we have an IRC channel set up at #drupal-crmcore.

Nov 16 2012
Nov 16

Trellon ran its first two CRM Core sprints and we accomplished a lot. We really want CRM Core to be a project shared and owned by a community of people with aligned goals for a CRM framework. By hanging in the IRC channel #drupal-crmcore, we'll be able to share best practices and pitfalls to avoid.

We'll be setting up crmcore.com in the near future as an exchange of sorts for new Features that people think would be useful to others. If you want to sign up for more information, we've set up a mailing list.

What did we get done in that first sprint?

  • We resolved free event registration bugs.
  • We fixed up some Entity API related errors that cropped up since API changes.
  • We set sensible default config for Simplenews module in install profile.
  • We Fixed news block text.
  • Newsletter delivery was tested.
  • We added social media links on news pages.
  • We added RSS for news and blog.
  • Commerce errors on donation pages were resolved.
  • There was an issue where views were being enabled before fields when enabling features. Not any more.
  • We improved path consistency for events, blog and news.
  • All features need standard logic for paths - we did that.
  • Code style was standardized in the donation feature.
  • We merged subscription report feature into news to ensure features are self containing.
  • String fixes.
  • Free events were triggering an unnecessary product product synch. We fixed that.
  • We replaced the crm core module in repo with git submodule to make staying up to date and contributing upstream easier.

For the second sprint:

  • We fixed an fatal error when donation feature is enabled.
  • The donation path was moved to admin/crm/donation/config and crm/donation.
  • We made a default payment processor "Dummy" for donation feature.
  • A warning message message for live donation page when using "dummy payment processor" was created.
  • We included a default donation page that comes out of the box.
  • A menu link was added to the default donation page.
  • We performed coder clean up on existing crm core features.
  • The news subscribe block for logged in users was fixed.
  • We refactored crm core profile activity so ajax callback works for whole profile activity settings form.
  • We fixed an issue of not deleting activity profile record.
  • We fixed issues related to activity fields checkboxes in crm core profile activity form.
  • And finally, a problem with de-serialization was addressed.

You can grab a copy of the install profile on Github here: https://github.com/Trellon/crmcoredemo. Trellon is holding a series of weekly virtual code sprints where we'll be extending and improving on it. You can join in now. Sign up on the mailing list and hang out in #drupal-crmcore!

Nov 15 2012
Nov 15

What is CRM Core?

CRM Core is a set of modules for managing contact records within a Drupal site, providing support for contacts, relationships and activities. It provides basic CRM system components and a framework for extending these components to build a custom system that will allow an organization to effectively meet their needs with respect to contact, relationship, and activity tracking.

What Does it do?

Out of the box CRM Core provides basic user interface to manage contacts, activities and relationships within the database. However, more importantly, it provides a set of common APIs to extend and build useful applications for your organization.

Designed to be extensible, CRM Core has been built with a pretty simple idea in mind: provide the basic tools everyone needs and avoid feature bloat. We really wanted to build something that can be used in the widest number of cases, without forcing people to deal with features they don't have a use for.

To that end, CRM Core is designed to allow people to build small, useful applications that are portable and can easily be shared just like the core modules. Using features, developers can easily export extensions to CRM Core to provide support for other ways of interacting with contacts. There are a number of features under development by Trellon, which we plan on releasing soon, and there are other developers in the community who are working with the core modules and plan for releases of their own.

Want to Help?

A Dev release of CRM Core is released to the community - you can grab a copy of the install profile on Github here: https://github.com/Trellon/crmcoredemo. Trellon is holding a series of weekly virtual code sprints where we'll be extending and improving on it. You can join in now. Hang out in #drupal-crmcore and chat with the team about where you can help out. xcf33 and pingers in channel can direct you to specific tasks that need effort.

Nov 07 2012
Nov 07

Introduction

Building a feature using CRM Core profile is easy. In this demo we will demonstrate the step-by-step instruction on how to do it. Please note this is a follow up and more in-depth guide to http://www.trellon.com/content/blog/building-crm-core-feature.

What are we building

In this demo we will be building an “online petition” feature that allows a website to:

  1. Create an online petition for a particular cause.
  2. Collect information from petition signers such as name, email, city and state.
  3. Run a report to see how many people have signed the petition and optionally export the data to a spreadsheet.
  4. Display a counter to show how many people have signed a specific petition.

Why don’t we use Web-form

Webform is a popular and powerful data collection tool in Drupal. It can be used to achieve the items that are laid-out above, so why not use it?

  • Insufficient views integration
    To show the actual webform submission data, additional modules (webform views, data module, etc have to be installed and configured)
  • Type of information it can collect
    Webform does not use entity-field API therefore it is missing the ability to collect complex data types such as phone number, address, etc.
  • Lack of normalization
    Webform does not differentiate if information should belong to a contact, an activity, or others. If we create another petition and some signers from previous petition signed the new one we will have to do data correlation to determine that information and link the two petition signatures.

What you need

To get started, your will need to have CRM Core modules running on your Drupal installation and have the CRM Core Profile module enabled. The easiest way to get started is to grab a quick CRM Core Demo installation profile from github.

You should also be familiar with the Features module and exportable objects. Knowledge of using CRM Core and views is recommended as well.

Building the components

Creating a “Petition” content type

We will first start by creating a content type called “Online petition”, this content type will hold information about any particular petition.

It will contain the following fields:

    Title
  • Body (Petition description)
  • Goal (integer) – A number we are looking to reach.
  • Image
  • Petition duration (date) – A date until which this petition will be active.

Verify contact fields exists

We mentioned that we would like to collect the name, email, city and state of the petition signer; that means those fields should exist in the contact type. If they do not exist let’s add them:

  • Contact Name: (a name field)
  • Email: (an email field)
  • Address: (postal address field)

In the example above, the “individual” contact type already has both a name field and an email field. It also has a billing address field, but we will create another field for the petition signing address.

Creating an “petition signature” activity type

The next step is to create a petition signature activity type which we will store the information about the petition signature. We will create the additional fields:

  • Public display (whether the petition signer gives consent to display his/her name on a public list of petition signers)
  • Petition Reference (A reference field back to the online petition content)

Create a CRM Core Profile form

Now we have created all of the components that will store online petition data. We have a content type, a contact type “individual” and an activity type “petition signature”.

The last step in configuration will be to create a CRM Core profile form, which will essentially be “the petition signing form”.

There are a couple of things we would like to customize on the form:

  1. We’d like to make the submit button say “Sign”
  2. We do not wish to collect the entire address; instead we only want to collect city and state information.

We will visit these 2 issues in the “Additional Customization” section below.

Export the feature

Now it’s time to export everything we have done into a “feature” module.

Additional Customization

Now that we have our “feature” exported, we can now implement additional customizations. We will do the following:

1. Permission
We want to introduce a permission so that only certain user roles can view the petition signing form. In this particular use case we don't have a great need for a permission, since we would like to allow everyone including anonymous users to be able to sign our petitions. Since many other use cases will require it, though, we include it for completeness.

<?php
// crm_core_petition.module /**
* Implements hook_permission
*/
function crm_core_petition_permission() {
  return array(
   
'crm_core sign online petition' => array(
     
'title' => t('Allow online petition signing'),
     
'description' => t('Ability to use petition signing form on online peition pages'),
    ),
  ); 
}
?>

2. Customize UI
The aforementioned customization on the form needs to be performed. We would also like to show the petition form on the petition content itself.

<?php
/**
* Implements hook_node_view
*/
function crm_core_petition_node_view(&$node, $view_mode) {
  
   if (!
user_access('crm_core sign online petition')) {
     return; 
   }   
// let's embed the petition signing form on the online petition content itself
 
if ($node->type == 'online_petition') {
   
$crm_core_profile = crm_core_profile_load('petition_signing_form');
    if (!empty(
$crm_core_profile)) {
     
module_load_include('inc', 'crm_core_profile', 'crm_core_profile.forms');
     
$node->content['crm_core_petition_form'] = drupal_get_form('crm_core_profile_entry_form', $crm_core_profile);     
     
$node->content['crm_core_petition_form']['#weight'] = 999;
    } 
  }
}
?>

The above code simply embeds the petition signing form we have created earlier into the petition content.

<?php
/**
* Implements hook_form_FORM_ID_alter().
*/
function crm_core_petition_form_crm_core_profile_entry_form_alter(&$form, &$form_state, $form_id) {
 
 
$profile = $form_state['profile'];   // making sure we are on the correct crm_core_profile
 
if ($profile['name'] != 'petition_signing_form') {
    return;
  }  
// adding css class to the form for additional style customization
 
if (empty($form['#attributes']['class'])) {
   
$form['#attributes']['class'] = array('crm_core_petition_signing_form');
  }
  else {
   
$form['#attributes']['class'] += array('crm_core_petition_signing_form');
  }
 
 
// Change the form button value and hide some of the address components
 
$form['submit']['#value'] = t('Sign the petition');
 
 
$form['field_contact_address'][LANGUAGE_NONE][0]['street_block']['#access'] = FALSE;
 
$form['field_contact_address'][LANGUAGE_NONE][0]['locality_block']['postal_code']['#access'] = FALSE;
 
$form['field_contact_address'][LANGUAGE_NONE][0]['country']['#access'] = FALSE;  
}
?>

We also do some UI customization such as adding CSS class to the form and hiding parts of the address field we don't need (street, locality, and country blocks).

3. Misc.
There are other things we would like to do such as setting a default value on the petition signature activity.

<?php
/**
* Implements hook_crm_core_profile_activity_alter().
*
* We are just setting some default value to the crm_core_profile form in the activity data container
* the $form here refers to the $form['activity'] data container from the original crm_core_profile form
*/
function crm_core_petition_crm_core_profile_activity_alter(&$form, $profile_name) { 
  if (
$profile_name != 'petition_signing_form') {
    return; 
  }
 
 
// since we are embedding the online petition form in the content itself, we can get the content information
  // as well
 
$node = menu_get_object();
 
 
// we are just setting the reference (association) of the activity back to the petition content
 
$default_value = sprintf('%s (%s)', $node->title, $node->nid);
 
$form['field_crm_petition_reference'][LANGUAGE_NONE][0]['target_id']['#default_value'] = $default_value;
}
?>

All done in less than 100 line of code. We can now test our feature.

Reporting and more

Now let’s create a simple report to show how many people have signed a particular petition as well as all the petition signers for a petition. We will also create a block counter to show the number of people who have signed the petition.

We will be using views and views data export module to achieve our goal.

Now that we have a general report showing a list of petitions created online, let’s create the petition signer report for an individual petition.

We now have a pretty good report showing all the signers of a petition as well. We can also create a petition signer count block and place it on the petition content:

Lastly, let’s update our feature with the report and other new components created.

That’s it, now you have a fully working online petition feature that can be deployed on any Drupal website with CRM Core and CRM Core Profile enabled.

Take it one step further

Let’s suppose that the following additional requirements have come in:

  1. We also want to track the source of the petition signers. We want to know if they came from an email campaign, a paid search engine campaign, or some other source.
  2. Upon successful signing of a petition, we want the user to be redirected to a customized thank you page where he/she can forward the petition to a friend, spread the word on social media, or take another action on the website.
  3. Suppose that we would like to make the petition time sensitive so they can only be signed in a specific time intervals and they will automatically become unavailable after the time has expired.

How would you go about implementing these additional requirements? Post your ideas in the comments, and we'll be happy to discuss them with you.

AttachmentSize 67 KB
Oct 03 2012
Oct 03

CRM Core is a tool for managing contact information within a Drupal website. It was designed to act as a platform, where developers can add small, useful applications on top of the core components to handle specific use-cases. It was also designed to be highly interoperable with Drupal so that people without any specialized programming knowledge could work with it.

CRM Core was launched a little over a year ago, and one of the most common questions we encounter is about creating a new feature to interact with the platform. Creating a CRM Core feature should not be a huge hurdle for anyone with a basic knowledge of how to work with Drupal. The process is pretty simple and should require minimal amounts of code; in fact, it's possible to create them without writing any code at all.

This blog post presents a step-by-step guide to creating CRM Core features within Drupal. It will demonstrate how someone can set up a site to collect information about contacts, create useful interfaces for interacting with the information that is collected, and build forms for collecting this information from the public. It also outlines how to create portable features for CRM Core that can be used across multiple sites, allowing people to build their own CRM systems to serve niche needs.

This exercise can be carried out by anyone with a fresh copy of Drupal, and can be completed in as little as 30 minutes. We have included sample code demonstrating all of the points in this exercise, in case you get stuck at any point or just wish to jump to the end.

What Are We Building

CRM Core is designed as a platform for building small, useful applications that add functionality to the system. The modules are designed with a fairly robust API to interact with, meaning a Drupal developer should be able to find lots of ways to hook into the system without much work. Developers are going to be building features, which should be familiar to most people with any kind of background with that module.

It's important to start off with a clear sense of what you are trying to accomplish. Stating the business case you are trying to capture can make all the difference in what you get out of building your feature. We suggest taking an approach where developers will start by building a small prototype that demonstrates what you want to accomplish, before going back to refine it over time using Drupal and CRM Core's APIs. This will save a lot of time by getting tools into your hands early and providing you with simple proofs of concept that can be efficiently built out into powerful tools through successive iterations.

In this case, let's say we are going to build a lead generation tool for tracking contacts who have come to the site and asked questions by filling out forms. In addition, we want to be able to manage leads by assigning them to specific users - our sales people - and ask them to follow up with each lead. It's pretty easy to assign further requirements from the get go, but this is a pretty simple list to get us started. What we are looking to get is a contact form for people to fill out, and an interface on the backend that allows people to see what leads have been submitted (and their status).

In CRM Core terms, we are going to be carrying out a number of simple steps to enable lead generation, focused on assembling a prototype that can be refined to make our new application very useful. To help you visualize the overall process:

  • Begin by installing Drupal and CRM Core.
  • Configure contact types and activity types for storing data.
  • Create a profile form for collecting information.
  • Customize the way information is handled in the system (optional).
  • Build a view to act as an interface for your data.
  • Export everything as a feature so you can encapsulate your code and share it with others.

There is very little in this list that basic site builders would find unfamiliar. The feature included with this message is an example you can look at in case you get stuck.

What You Need

To begin with, you need to install CRM Core and it's dependencies. The basic requirements for CRM Core are listed below, and there is not much configuration required for any of them to work. Using a fresh installation of Drupal, install and activate the following modules:

  • Views
  • CTools
  • CRM Core
  • CRM Core UI
  • CRM Core Activity
  • CRM Core Activity UI
  • CRM Core Contact
  • CRM Core Contact UI
  • CRM Core Relationship
  • CRM Core Relationship UI
  • Entity API
  • Entity Reference
  • Relation
  • Field UI
  • Image
  • File
  • Address Field
  • Email
  • Link
  • Phone Number
  • Fieldgroup
  • Name Field
  • Date API
  • Date Popup
  • Date Views

This may sound like a long list, but remember: a lot of these modules are simply field definitions, and some of them are things you would need elsewhere in your site regardless of whether or not CRM Core was installed.

In addition to the basic requirements, install CRM Core Profile (http://drupal.org/project/crm_core_profile). This module is important, it will allow you to create contact forms that can be displayed anywhere within your Drupal site for collecting information. We are going to use it in this example to create one form for capturing contact information, and it can be used over and over again to collect lots of information from your users.

Step One: Configure your Contact Types

To begin with, you will want to configure your contact types so there is a way to store information about contacts. This can be done through the web-based admin interface by going to Structure > CRM > CRM Contact Types, or through the following path: admin/structure/crm/contact-types.

Select the individual contact type and start configuring fields. There's not much to say about this page, administrators will configure fields for collecting contact data in the same manner he or she would for a content type. This page allows you to specify the information you are going to track about conations coming to your site. You can add an unlimited number of fields from this page, and you will likely come back to it at some point to set up more fields to collect about people.

For this exercise, make sure there are fields for name, image, email address, address, and phone number. These will be the basic fields we use to collect information from users in the site. Feel free to add additional fields, but they are not required to complete this exercise.

Step Two: Create an Activity Type

The next step is to configure an activity type that will be used to track information submitted by users. We are going to configure the system to capture leads, and to associate them with specific contacts so you can track them over time.

In case you are wondering, an activity type is simply a way of tracking action for a contact. In CRM Core terms, this is the basic way to track interaction with someone, it stores information about the interaction as opposed to information about the person. When you plan for a CRM Core installation, it's important to think about the ways you are going to want to track people and set up the appropriate activities to capture information efficiently. This is a page you will probably come back to each time you need to track something new about interaction with people.

Begin by going to Structure > CRM > CRM Activity types in the web based administrative interface, or simply by going to the path admin/structure/crm/activity-types.

From here, create a new activity type called "Lead" and create some fields for that activity type. There will be some default fields already configured. Leave those alone, and create some additional fields that will be used to capture information. For the purpose of this exercise, set up the following custom fields using the web-based administrative tool:

  • Status - to track the state of the lead
  • Assigned To - to track who is responsible for the lead. Use entity reference to associate the activity and a user.

Like contacts, activities can store any number of custom fields you need to effectively track information. You can associate any additional fields required as part of this activity type, and it can easily be expanded to capture other points of data over time. For right now, we simply want to know that leads are being acted on, and that someone is responsible for them. Save the activity and continue.

Step Three: Export the Activity Type to a Feature Module

A best practice with CRM Core is to export your customizations to features. On the one hand, this makes your new application portable, meaning you can share it with others and use it on multiple websites. On the other hand, it helps to keep your work organized and free from defects that can emerge over time. It's a good idea to export features early and often, and occasionally test them using another installation of Drupal.

Within the web-based administrator, go to Structure > Features and create a new feature called 'CRM Core Leads.' From within the components drop down, select the crm_core_activity_type option, and check off the box for Lead. This will create a feature that simply stores our new activity type. We will add additional information to it over time.

Export the feature and save it in your sites directory, preferably somewhere like sites/all/modules/features/crm_core_leads. Afterwards, return to the features page and make sure the new feature you just created is enabled.

Step Four: Create a crm_core_profile Form

At this point, your site has the ability to store contacts, and associate leads with them. The next step is to create the contact form that will be used to capture the lead and store it in the system.

is a module that builds forms to collect information about contact and activities. It's a very powerful tool for collecting information, and can be used to create any number of forms in your site that populate fields in contact and activity records. The CRM Core API can be used along with CRM Core Profile to efficiently handle very complex business requirements. The module itself contains many examples of how to write code for CRM Core Activities that can be used as templates for other modules.

In this example, we are simply going to use the out of the box components to create a new contact form, which will drive the creation of leads within CRM Core.

Begin by going to the Structure > CRM > CRM Core Profile in the web-based administrator, or directly to the path admin/structure/crm/profile. This will present you with a list of all profile forms that are configured for your system. Click the link to add a new profile at the top of the screen.

Start by giving the new profile the name Lead Inquiry. Note the machine name being generated next to the title of the form. Programmers will use this when they need to reference the profile from Drupal's API, and we will show you an example of how to do this in this exercise.

You will be presented with a screen that allows you to select the fields you want to use to populate the contact record and the activity. Select the following fields from the Contact Record:

  • Name
  • Email
  • Phone Number

In addition to the basic fields, you can provide some information to help users access your new profile through this screen as well. Optionally, you can provide a path to get to the form, which will allow public access to the form from the specified URL. You can provide a redirect path to take users to a new URL after submitting the form. You can enter a message to display when users successfully submit the form. You can also enable a block to display the form within your site. These are not necessary for the purposes of this exercise, but may help you to visualize the results.

Below, in the activity information section, use the select box to select the Lead activity type. The screen will refresh with a list of fields. Select the description box, this will simple be used to collect some notes about the activity.

Click the create button. At this point, you will have a form that allows you to collect leads within your site. You can see it listed on the CRM Core Profile screen in the web-based admin screen, and can browse to it via the path specified in the configuration screen.

Step Five: Add Customization in the Feature Module

This step is completely optional. If code makes you all upset, skip to step six.

One of the challenges that comes from creating new activity types is identifying individual ones in the system. Over time, contact records can generate a lot of activities. It's important to lay out activities n a meaningful way, in order to ensure you will be able to understand what has happened with a contact over time.

We need to do some scripting to people can easily identify a lead in the system. As part of this exercise, we are going to add some code to the feature module we created for our new lead generation tool, which is going to add a distinct name for each lead that comes in.

Open crm_core_leads.module from within your feature and insert the following code:

<?php
/**
* Implements hook_crm_core_profile_activity_alter
*/
function crm_core_lead_crm_core_profile_activity_alter(&$form, $profile_name) { 
  if (
$profile_name == 'lead_inquiry') {
   
$form['title']['#value'] = t('Contact Form Lead');
  }
}
?>

In Drupal terms: we are implementing a hook which will override and change the name of the activity to "Contact Form Lead" when it is being associated with the contact record. Every new lead associated with the contact record will have the same name, "Contact Form Lead." Optionally, if you wanted some additional control over the name, a developer could include any other information from the profile form in the title (for instance, a date or the name of the contact). For practical purposes, this is all that is needed.

It's important to note that this example does not use CRM Core's API or any special code outside of Drupal's APIs. A developer does not really need to understand anything specific to CRM Core in order to make this work, but it helps to know why are doing it. There are many other ways to extend and customize this feature module, but the above is all that is useful for this exercise.

Go ahead and export your feature again at this point. While it's not strictly necessary, it's a good idea to export a feature any time you have added new code or changed any settings associated with it, for the reasons stated previously.

Step Six: Add Some Administrative Screens

At this stage, we have our contact records configured, an activity type for tracking information, and a profile form for collecting information. We have customized the feature to carry out some basic operations that allow users to identify their leads more easily in the system. The only thing really left is to give people a simple way to interact with their leads by creating an interface to track them.

As part of this exercise, we are going to create a screen within our feature that allows people to lookup leads, change their status, and assign leads to specific users within our system. The simplest way to create such an interface is the views module, which will be used to present lists of leads and operate on them within the site.

Create a new view with the following basic setup:

  • Name the view 'CRM Core Leads.'
  • Use select Activity Types from the show select menu, and select activity types of 'Lead.'
  • Check the box to create a page for the view, call it 'All Leads,' and give it a path of 'all_leads'.
  • For display format, have it display a table.

Press the continue and edit button. On the resulting page, add fields for the contact and the activity type as part of this exercise. Views will give us a list of fields referencing the activity itself, and we are going to add relationships to get information about the associated contact.

Within the advanced menu, click the link to add a relationship. Check the box for 'Entity Reference: Referenced Entity' and apply it to all displays. On the resulting screen, just select the option to apply it to all displays. This will allow you to add fields to the view for the contact associated with the lead.

From here, add fields to the view. For this exercise, we are going to add the following:

  • Name (from crm_core_contact:individual)
  • Email address (from crm_core_contact:individual)
  • Status (from CRM Activity)
  • Assigned To (from CRM Activity)
  • Date (from CRM Activity)
  • Edit Link (from CRM Activity)

With these fields in place, turn off the page and save the view.

This view can be customized to include any fields you are looking for. Just remember, the relationship is important, it is what lets you reference content from the actual contact record. You can use this along with aggregation to get some really interesting data into your interface.

Once your new view is created, save it along with the CRM Core Leads feature, and export the view.

Step Seven: Extend Your Feature

This exercise provides an overview of the basic method for creating a new feature for CRM Core. This feature provides an interface for users to look at information related to a contact, tracks leads along with status and assignment, and stores information in manner where it can easily be identified within the system.

From here, it's up to you to extend it to handle the specific business rules for your website. There are a lot of ways to go from here, and you may want to consider some of the following as paths for how to make the site awesome:

  • Securing the view, assigning the appropriate permissions to make sure only the right people can access it.
  • Implementing views bulk operations, to allow you to modify a bunch of leads all at once.
  • Implementing exposed filters within the view, to limit the number of records returned or for only viewing leads assigned to a specific user.
  • Implementing additional views to show how many leads have come in in a given month.
  • Implement filters within the views to ensure users can only access their own leads.
  • Add triggers around the submission of the profile form to receive emails whenever you receive a new lead.
  • Associate contact records with users in your Drupal site to allow people to self-maintain their contact information.
  • Associate your CRM Core profile forms with other content types, in order to allow in-line submissions.
  • Create organic groups to manage large lists of contacts.
  • Repeat this exercise for other types of interaction with users besides creating leads.

Take the time to consider how long it took to carry out this exercise. In general, adding a new feature to CRM Core is the sort of activity a developer can handle in less than a day to get to a prototype stage. A best practice around CRM Core development is to build features quickly before going back to refine specific interface and business rule requirements. Something we like to say at Trellon, no one ever knows exactly what they want until they have their hands on it.

A word of advice: take features seriously. At some point, we are going to set up a features exchange for CRM Core, to allow people to share the features they create with the community. For right now, it's helpful for developers to maintain a stock of the features they have created to keep handy for when they are building sites. Trellon has a set of features for event management, membership, claiming profiles, controlling access to physical resources, donations, store purchases and more. We plan on releasing them along with the features exchange, and will be happy to share them before it is ready if you ask us nicely.

Oct 02 2012
Oct 02

Berkeley + Early November = BADCamp.

I'm a big fan of camps - I have helped organize quite a few in Colorado. They help cement local communities. With the cross pollination of camp goers, they provide ways for those communities to interact with one another. Many of the camps have become conferences in their own right - with BADCamp being one of the largest.

Trellon is a core sponsor this year. Our hope is to lead a few sessions this year. We could use your votes!

  • DRUPAL'S MOBILE APP GENERATOR AND YOU: LEARNING HOW TO LOVE THE SMARTPHONE / TABLET
    Do you often think about making your mobile Drupal website friendly for smartphones? Or have you already started to build a Drupal mobile application using MAG (Mobile App Generator) module and PhoneGap? If the answer to either of these questions is yes, you should attend this session!
  • CONTACT MANAGEMENT IN DRUPAL: GETTING RESULTS WITH CRM CORE
    This session will present 8 things you can do with CRM Core to manage contact relationships and accelerate your site development. We will also focus on ways you can extend it to handle use-cases specific to your business needs.
  • AGILE GYMNASTICS AND TIMEBOX TUMBLING
    Project management requires a blend of techniques and tools to effectively shepherd projects from ideation to release. We'll explore and discuss different tools and methodologies that can help make your project successful.

These presentations were shared in Munich. The presentation on the agile process was also one of the keynotes at last year's Drupalcamp Austin and was updated and presented in Denver.

Vote for the Trellon sessions and then come learn about the interesting things the team has been up to!

AttachmentSize 21.36 KB
Sep 28 2012
Sep 28

After decompressing for a couple of weeks from Drupalcon Munich, some observations have filtered to the surface. This is my 10th Drupalcon. It seems to be hard to believe. I've seen changes to the demographics of the convention that I think are worth sharing.

Our age

Back in 2007 in Barcelona, the average age of the convention go-er really seemed to be about 28 years old. We had some attendees that were older (like me) some that were younger, but by in large we were looking at a group of folks that were not at the beginning of their careers, but certainly not elder statesmen.

5 years later, the average age seems to be around 36-37ish. Again, some younger and some older, but the fascinating thing is that we seem to be aging, as a group faster than 1 year for 1 year. This seems to indicate that we are attracting professionals that are further along in their careers.

Is this good or bad?

Well, it swings both ways. Older community members bring stability and wisdom to the project. Still, like any population, you want enough young folks coming into the fold to replace those that leave. Attrition is a killer and open source software projects like ours can suffer from the equivalent of "HR attrition". Attrition in our community occurs when:

  1. People burn out and stop contributing
  2. People move to another technology
  3. People die

This is natural and to be expected. The challenge is replacing those individuals. They cover the full gambit of workers ranging from coders, to themers, to site builders, to product managers, to project managers, to business developers, to executives.

If we are attracting more mature people to our community, we need to do more to attract younger people into the community. This is critical for the sustainability of the project as a whole.

Our gender

We're doing pretty well on this count. Back in 2007 in Barcelona, the breakdown between men and women seemed to be about 3% women and 97% men. The Drupalchix Meetup at the Con in Barcelona was about twelve people. In other words, women were so under represented it was almost absurd. According to Geek Feminism, in 2007 opensource only had 1.5% representation from women. The technology industry as a whole has 10-30% representation by women.

At the time of Drupalcon Munich, the percentage of women was up to 17% in the Drupal project. The "t-shirt report" in Munich showed 79% men and 11% of women in attendance. This isn't scientific, but shows that no fewer than 11% of attendees were women. This is still not good enough. We need to continue to attract women to the project.

Our growth

If Drupalcons are a slice of the community as a whole, then we have flatlined in our growth. The last couple of American Drupalcons have had very little growth in the number of attendees. In Europe, there continues to be modest growth. Some have argued that this is due to venue size - but my sense given the curve of when the ticket countdown occurs, we would find we just couldn't sustain larger numbers.

Drupalcon Attendance
Growth in Drupalcon Attendance

This points back to our need to more effectively recruit new people.

Recently, on the Drupal Marketing Group, I wrote about our need to diversify. That, as a community, we need to embrace all our cultures. By extension we need to recruit more youth. We need to continue to be inclusive of women. We need to reflect, demographically, the population we want to serve. It should be diverse in age, color, and gender. There is still a lot of work to be done.

AttachmentSize 43.13 KB 23.13 KB
Mar 21 2012
Mar 21

CRM Core is a set of modules for managing contact records within a Drupal site, providing support for contacts, relationships and activities. It provides the basic tools organizations need to effectively track the relationships they maintain with people, along with a framework for extending that support to handle other use cases.

Our team has been working on CRM Core the release of Drupal 7, keeping in mind the idea that Drupal now has the potential to be a lot more than a CMS. Originally started as a proof of concept, this set of modules has grown to become a solution we have used when working with groups that don't work well with traditional CRM approaches. Now that it is being used by a number of organizations, we wanted to share some information about the system and how you can put it to work as a solution for managing contact information.

What is it and What Does it Do?

A common challenge many organizations face is handling personal information collected over the web. While it's fine to simply store contact information in a table or email, these approaches don't really provide a lot of value to people looking to understand the way others work with their group over time. Sophisticated contact relationship management tools, called CRM systems, are commonly employed to centralize the collection of information about people interacting with an organization, and allow it to be presented in a useful form.

We prepared a video that demonstrates the basic tools that come with CRM Core, to showcase what you get out of the box when installing the modules.

In this video, Chang Xiao, one of Trellon's technical leads, shows many of the key features that exist in the platform. CRM Core ships with support for managing contact records, activities, and relationships, along with some basic search tools that can be refined using Views to accomodate more sophisticated reporting needs. All of these are shown in the video, along with some of the tools that are in place for customizing the system to create your own contact, activity and relationship types.

Why Doesn't It Support X Feature?

Designed to be extensible, CRM Core has been built with a pretty simple idea in mind: provide the basic tools everyone needs and avoid feature bloat. We really wanted to build something that can be used in the widest number of cases, without forcing people to deal with features they don't have a use for.

To that end, CRM Core is designed to allow people to build small, useful applications that are portable and can easily be shared just like the core modules. Using features, developers can easily export extensions to CRM Core to provide support for other ways of interacting with contacts. There are a number of features under development by Trellon, which we plan on releasing soon, and there are other developers in the community who are working with the core modules and plan for releases of their own.

For more information and getting involved with the project, please visit http://drupal.org/project/crm_core.

Feb 03 2012
Feb 03

There have been plenty of articles about Drupal 7 and entities already. Entities are great. They provide a unified way to work with different data units in Drupal. Drupal 7 is all about entities. They are everywhere: nodes, users, taxonomy terms, vocabularies...

But how, as developers, can we create our own entities? When do we really need to do that? I think these questions are really very project-specific. We can probably use nodes for nearly everything. But when it comes to performance-sensitive projects, nodes should really only be used for content, and we should separate as much as possible from nodes. Why? Nodes are revisioned, they fire a lot of hooks, and they have functionality that we likely won't need. Also if we know exactly what fields we should have in our entities, we can create our own custom entities to avoid all those joins of Field API tables.

There are plenty of examples in Drupal core of how to create entities. To create entities, you should really write a lot of code. But there is also, of course, a faster way. You can use Entity API to simplify the code structure. With Entity API we can really move a lot of code to Entity Controller using standard Entity API functions and, in this way, keep our main module clean and nice.

Entity API provides:

  • More functions to work with entities. Drupal core provides functions to work with entities: entity_load(), entity_label(), entity_extract_ids(), entity_uri(), entity_get_info(). Core doesn't have any functions to save or delete entities, but this is where Entity API helps us. It provides: entity_id(), entity_export(), entity_import(), entity_save(), entity_create(), entity_delete(), entity_view(), entity_access(), entity_get_property_info(). This allows us to move more logic for all operations to the Controller Class (we will talk about this bit later).
  • Administration UI for exportable entities. This is a really great feature that allows us to create UIs for exportable entities easily. This is needed when we have bundles as entities. The administrative interface is very similar to the content types overview page for nodes.
  • Metadata wrappers. This allows us nicely get/set values of properties and fields of entities. Also we can get/set data of related entities... like geting the value of a profile field for the author of the node. No more LANGUAGE_NONE everywhere in the code!
  • Integration with Views and Rules. Integration with Rules is done by default when we use the Entity API Controller class, so we can hook in during creating/editing/deleting entities. By default Views can create a view using properties of our Entities, but they are very generic. After we describe the properties to Entity API (this is also needed for wrappers), Views will understand much more. Then we will be able to build different kinds of relationships, exposed filters, etc.
  • Provide entity tokens (entity_token module).

Administration UI screenshot

Every entity has its own Controller. This class is responsible for all operations on this particular entity (create, edit, delete, view, etc). This is very convenient as it can be easily reused by our custom entities, thanks to inheritance. It's also worth mentioning that with Entity API, our entities are "first class" objects and not objects of stdClass.

Lets take a look at a practical implementation of an entity to sample the functionality.

Practical example

Our case is quite abstract: we create two entities, example_task and example_task_type. Task has the properties: uid (author of the task), type, created, changed, title, and description. Task type is simply information about bundles of the task, so it has the fields: id, type, label, and description. Other fields of the Task type are added by Entity API to make this entity exportable (module and status properties).

First of all, (as in case with using only core functionality) in order to implement custom entities we should describe their base tables in hook_schema(). I will avoid listing of all that code here.

Then we should implement hook_entity_info() to declare our entities:

<?php
/**
* Implements hook_entity_info().
*/
function example_task_entity_info() {
 
$return = array(
   
'example_task' => array(
     
'label' => t('Task'),
     
'entity class' => 'ExampleTask',
     
'controller class' => 'ExampleTaskController',
     
'base table' => 'example_task',
     
'fieldable' => TRUE,
     
'entity keys' => array(
       
'id' => 'tkid',
       
'bundle' => 'type',
      ),
     
'bundle keys' => array(
       
'bundle' => 'type',
      ),
     
'bundles' => array(),
     
'load hook' => 'example_task_load',
     
'view modes' => array(
       
'full' => array(
         
'label' => t('Default'),
         
'custom settings' => FALSE,
        ),
      ),
     
'label callback' => 'entity_class_label',
     
'uri callback' => 'entity_class_uri',
     
'module' => 'example_task',
     
'access callback' => 'example_task_access',
    ),
  );
 
$return['example_task_type'] = array(
   
'label' => t('Task Type'),
   
'entity class' => 'ExampleTaskType',
   
'controller class' => 'ExampleTaskTypeController',
   
'base table' => 'example_task_type',
   
'fieldable' => FALSE,
   
'bundle of' => 'example_task',
   
'exportable' => TRUE,
   
'entity keys' => array(
     
'id' => 'id',
     
'name' => 'type',
     
'label' => 'label',
    ),
   
'module' => 'example_task',
   
// Enable the entity API's admin UI.
   
'admin ui' => array(
     
'path' => 'admin/structure/task-types',
     
'file' => 'example_task.admin.inc',
     
'controller class' => 'ExampleTaskTypeUIController',
    ),
   
'access callback' => 'example_task_type_access',
  );   return
$return;
}
?>

I won't describe every option in hook_entity_info() here as there is a lot of documentation and articles about it already (see the list of references in the bottom of this article).

What is specific to Entity API:

  • we define 'entity class'. It is self explanatory.
  • 'label callback' => 'entity_class_label'. This is a standard Entity API label callback that takes a look at our entity class method defaultLabel()
  • 'uri callback' => 'entity_class_uri'. This is also a standard Entity API callback for uri. It executes our entity defaultUri() method
  • 'exportable' key for example_task_type. In our example we set task types to be exportable.
  • 'admin ui' for example_task_type. This is where we define our Administration UI for task types.

The next step in our implementation is to let Drupal understand that entities of example_task_type are bundles for example_task. This should be done in following way:

<?php
/**
* Implements hook_entity_info_alter().
*/
function example_task_entity_info_alter(&$entity_info) {
  foreach (
example_task_types() as $type => $info) {
   
$entity_info['example_task']['bundles'][$type] = array(
     
'label' => $info->label,
     
'admin' => array(
       
'path' => 'admin/structure/task-types/manage/%example_task_type',
       
'real path' => 'admin/structure/task-types/manage/' . $type,
       
'bundle argument' => 4,
      ),
    );
  }
}
?>

Next we should take care about pages where we add, view, and edit our example tasks. To do this we define various items in hook_menu():

<?php
/**
* Implements hook_menu().
*/
function example_task_menu() {
 
$items = array();   $items['task/add'] = array(
   
'title' => 'Add task',
   
'page callback' => 'example_task_admin_add_page',
   
'access arguments' => array('administer example_task entities'),
   
'file' => 'example_task.admin.inc',
   
'type' => MENU_LOCAL_ACTION,
   
'tab_parent' => 'task',
   
'tab_root' => 'task',
  );  
$task_uri = 'task/%example_task';
 
$task_uri_argument_position = 1;   $items[$task_uri] = array(
   
'title callback' => 'entity_label',
   
'title arguments' => array('example_task', $task_uri_argument_position),
   
'page callback' => 'example_task_view',
   
'page arguments' => array($task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('view', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.pages.inc',
  );  
$items[$task_uri . '/view'] = array(
   
'title' => 'View',
   
'type' => MENU_DEFAULT_LOCAL_TASK,
   
'weight' => -10,
  );  
$items[$task_uri . '/delete'] = array(
   
'title' => 'Delete task',
   
'title callback' => 'example_task_label',
   
'title arguments' => array($task_uri_argument_position),
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_delete_form', $task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('edit', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.admin.inc',
  );  
$items[$task_uri . '/edit'] = array(
   
'title' => 'Edit',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_form', $task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('edit', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.admin.inc',
   
'type' => MENU_LOCAL_TASK,
   
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  );   foreach (
example_task_types() as $type => $info) {
   
$items['task/add/' . $type] = array(
     
'title' => 'Add task',
     
'page callback' => 'example_task_add',
     
'page arguments' => array(2),
     
'access callback' => 'entity_access',
     
'access arguments' => array('create', 'example_task', $type),
     
'file' => 'example_task.admin.inc',
    );
  }  
$items['admin/structure/task-types/%example_task_type/delete'] = array(
   
'title' => 'Delete',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_type_form_delete_confirm', 4),
   
'access arguments' => array('administer example_task types'),
   
'weight' => 1,
   
'type' => MENU_NORMAL_ITEM,
   
'file' => 'example_task.admin.inc',
  );   return
$items;
}
?>

Of note in the hook_menu() implementation is that we use entity_access for most of the access callbacks. The purpose of this function is to check for an access callback defined by the entity ('access callback' property) and execute it. So our access callback for tasks looks like this:

<?php
/**
* Access callback for Task.
*/
function example_task_access($op, $task, $account = NULL, $entity_type = NULL) {
  global
$user;   if (!isset($account)) {
   
$account = $user;
  }
  switch (
$op) {
    case
'create':
      return
user_access('administer example_task entities', $account)
          ||
user_access('create example_task entities', $account);
    case
'view':
      return
user_access('administer example_task entities', $account)
          ||
user_access('view example_task entities', $account);
    case
'edit':
      return
user_access('administer example_task entities')
          ||
user_access('edit any example_task entities')
          || (
user_access('edit own example_task entities') && ($task->uid == $account->uid));
  }
}
?>

And of course our hook_permissions() for defining our permissions

<?php
/**
* Implements hook_permission().
*/
function example_task_permission() {
 
$permissions = array(
   
'administer example_task types' => array(
     
'title' => t('Administer task types'),
     
'description' => t('Allows users to configure task types and their fields.'),
     
'restrict access' => TRUE,
    ),
   
'create example_task entities' => array(
     
'title' => t('Create tasks'),
     
'description' => t('Allows users to create tasks.'),
     
'restrict access' => TRUE,
    ),
   
'view example_task entities' => array(
     
'title' => t('View tasks'),
     
'description' => t('Allows users to view tasks.'),
     
'restrict access' => TRUE,
    ),
   
'edit any example_task entities' => array(
     
'title' => t('Edit any tasks'),
     
'description' => t('Allows users to edit any tasks.'),
     
'restrict access' => TRUE,
    ),
   
'edit own example_task entities' => array(
     
'title' => t('Edit own tasks'),
     
'description' => t('Allows users to edit own tasks.'),
     
'restrict access' => TRUE,
    ),
  );   return
$permissions;
}
?>

Now let's take a look at the most interesting part – controllers. The Task entity class looks like this:

<?php
/**
* Task class.
*/
class ExampleTask extends Entity {
  protected function
defaultLabel() {
    return
$this->title;
  }   protected function
defaultUri() {
    return array(
'path' => 'task/' . $this->identifier());
  }
}
?>

We only define our label for the entity and what path it should have.

Now Controller class:

<?php
class ExampleTaskController extends EntityAPIController {   public function create(array $values = array()) {
    global
$user;
   
$values += array(
     
'title' => '',
     
'description' => '',
     
'created' => REQUEST_TIME,
     
'changed' => REQUEST_TIME,
     
'uid' => $user->uid,
    );
    return
parent::create($values);
  }   public function
buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
   
$wrapper = entity_metadata_wrapper('example_task', $entity);
   
$content['author'] = array('#markup' => t('Created by: !author', array('!author' => $wrapper->uid->name->value(array('sanitize' => TRUE)))));     // Make Description and Status themed like default fields.
   
$content['description'] = array(
     
'#theme' => 'field',
     
'#weight' => 0,
     
'#title' =>t('Description'),
     
'#access' => TRUE,
     
'#label_display' => 'above',
     
'#view_mode' => 'full',
     
'#language' => LANGUAGE_NONE,
     
'#field_name' => 'field_fake_description',
     
'#field_type' => 'text',
     
'#entity_type' => 'example_task',
     
'#bundle' => $entity->type,
     
'#items' => array(array('value' => $entity->description)),
     
'#formatter' => 'text_default',
     
0 => array('#markup' => check_plain($entity->description))
    );     return
parent::buildContent($entity, $view_mode, $langcode, $content);
  }
}
?>

Please pay attention to the fact that we inherit our Controller class from EntityAPIController class that is provided by Entity API.

The create() method is responsible for the default values of entity properties when creating a new object. It is imperative that our entity has values when saving the object. In order to create an 'empty' entity we use the entity_create function.

buildContent() is needed for us to have our Description field displayed on the view page for the entity. We show this field using a theming function from the Field API, so the behaviour will be the same as other fields added with fields. Also you can see usage of a wrapper here, but lets come back to it a bit later.

Controller and entity class for example task type look different:

<?php
/**
* Task Type class.
*/
class ExampleTaskType extends Entity {
  public
$type;
  public
$label;
  public
$weight = 0;   public function __construct($values = array()) {
   
parent::__construct($values, 'example_task_type');
  }   function
isLocked() {
    return isset(
$this->status) && empty($this->is_new) && (($this->status & ENTITY_IN_CODE) || ($this->status & ENTITY_FIXED));
  }
} class
ExampleTaskTypeController extends EntityAPIControllerExportable {
   public function
create(array $values = array()) {
   
$values += array(
     
'label' => '',
     
'description' => '',
    );
    return
parent::create($values);
  }  
/**
   * Save Task Type.
   */
 
public function save($entity, DatabaseTransaction $transaction = NULL) {
   
parent::save($entity, $transaction);
   
// Rebuild menu registry. We do not call menu_rebuild directly, but set
    // variable that indicates rebuild in the end.
    // @see _http://drupal.org/node/1399618
   
variable_set('menu_rebuild_needed', TRUE);
  }
}
?>

Please pay attention to the fact that we inherit our Controller class from EntityAPIControllerExportable class that is provided by Entity API. This is needed to make our entity exportable.

For entity class we define the method isLocked(). This is needed to understand whether this entity is locked so it is not possible to edit/delete it.

In the Controller class we also define the create() method for an empty entity and in the save() method we should call field_attach_create_bundle() to let Fields API know that a new bundle is created. Of course, we also rebuild the menu to refresh our newly created bundle admin paths.

Now we need to define our forms for saving/deleting entities. You can find them in attached module. There is nothing non standard compared to not using Entity API there so let's skip this part (there is plenty of code there as well).

Instead lets take a closer look at usage of other standard Entity API functions in our module.

What we are missing is an API to work with our entities. Here it is:

<?php
/**
* Load a task.
*/
function example_task_load($tkid, $reset = FALSE) {
 
$tasks = example_task_load_multiple(array($tkid), array(), $reset);
  return
reset($tasks);
}
/**
* Load multiple tasks based on certain conditions.
*/
function example_task_load_multiple($tkids = array(), $conditions = array(), $reset = FALSE) {
  return
entity_load('example_task', $tkids, $conditions, $reset);
}
/**
* Save task.
*/
function example_task_save($task) {
 
entity_save('example_task', $task);
}
/**
* Delete single task.
*/
function example_task_delete($task) {
 
entity_delete('example_task', entity_id('example_task' ,$task));
}
/**
* Delete multiple tasks.
*/
function example_task_delete_multiple($task_ids) {
 
entity_delete_multiple('example_task', $task_ids);
}
?>

As you can see, these are simply wrappers around Entity API functions. This is a great benefit of using Entity API as we make our API very simple and transparent.

The same applies for our Task type entity:

<?php
/**
* Load task Type.
*/
function example_task_type_load($task_type) {
  return
example_task_types($task_type);
}
/**
* List of task Types.
*/
function example_task_types($type_name = NULL) {
 
$types = entity_load_multiple_by_name('example_task_type', isset($type_name) ? array($type_name) : FALSE);
  return isset(
$type_name) ? reset($types) : $types;
}
/**
* Save task type entity.
*/
function example_task_type_save($task_type) {
 
entity_save('example_task_type', $task_type);
}
/**
* Delete single case type.
*/
function example_task_type_delete($task_type) {
 
entity_delete('example_task_type', entity_id('example_task_type' ,$task_type));
}
/**
* Delete multiple case types.
*/
function example_task_type_delete_multiple($task_type_ids) {
 
entity_delete_multiple('example_task_type', $task_type_ids);
}
?>

I hope now you start to have a feeling how convenient it is to use standard Entity API functions to build your own entities.

Now let's take a look at metadata wrappers which simplify getting and setting values of entity properties/fields. E.g.

<?php
// Create wrapper around the node.
$wrapper = entity_metadata_wrapper('node', $node); // We can do it also using only $nid.
$wrapper = entity_metadata_wrapper('node', $nid); // Get the value of field_name of the nodes author's profile.
$wrapper->author->profile->field_name->value();
$wrapper->author->profile->field_name->set('New name'); // Value of the node's summary in german language.
$wrapper->language('de')->body->summary->value(); // Check whether we can edit node's author email address.
$wrapper->author->mail->access('edit') ? TRUE : FALSE; // Get roles of node's author.
$wrapper->author->roles->optionsList(); // Set description of the node's first file in field field_files.
$wrapper->field_files[0]->description = 'The first file';
$wrapper->save(); // Get node object.
$node = $wrapper->value();
?>

The entity definition informs meta data wrappers about the raw data structure(base table definition), but this doesn't describe the context of a field's use. For example, the 'uid' field from our task entity is a simple integer and Drupal doesn't know that it is a reference to a user object. In order to create this link we use following code:

<?php
/**
* Implements hook_entity_property_info_alter().
*/
function example_task_entity_property_info_alter(&$info) {
 
$properties = &$info['example_task']['properties'];
 
$properties['created'] = array(
   
'label' => t("Date created"),
   
'type' => 'date',
   
'description' => t("The date the node was posted."),
   
'setter callback' => 'entity_property_verbatim_set',
   
'setter permission' => 'administer nodes',
   
'schema field' => 'created',
  );
 
$properties['changed'] = array(
   
'label' => t("Date changed"),
   
'type' => 'date',
   
'schema field' => 'changed',
   
'description' => t("The date the node was most recently updated."),
  );
 
$properties['uid'] = array(
   
'label' => t("Author"),
   
'type' => 'user',
   
'description' => t("The author of the task."),
   
'setter callback' => 'entity_property_verbatim_set',
   
'setter permission' => 'administer example_task entities',
   
'required' => TRUE,
   
'schema field' => 'uid',
  );
}
?>

We also define 'date' type for our 'created' and 'changed' fields. Now we can use the following way to retrieve the name of our entity author:

<?php
$wrapper
= entity_metadata_wrapper('example_task', $entity);
$wrapper->uid->name->value(array('sanitize' => TRUE));
?>

We can also build views relationship to our entity author. See the screenshot below.


Revisions

At the moment entity revisions are not supported by Entity API. There are steps being made to include this functionality in issue http://drupal.org/node/996696, so your help to move this issue forward is very welcome.

Other examples and documentation

I hope it has become clearer how to simplify creating entities using Entity API. There are a lot of modules that leverage the EntityAPI already like Profile2 and DrupalCommerce.

I would like to thank Wolfgang Ziegler (fago) a lot for this great module and all his work.

References to great resources about Entities and Entity API module:

AttachmentSize 7.79 KB
Nov 14 2011
Nov 14

The Apachesolr module (7.x) allows us to build sites that use powerful search technology. Besides being blazingly fast, Apachesolr has another advantage: faceted search.

Faceted search allows our search results to be filtered by defined criteria like category, date, author, location, or anything else that can come out of a field. We call these criteria facets. With facets, you can narrow down your search more and more until you get to the desired results.

Sometimes, however, the standard functionality is not enough. You might want or need to customize the way that facets work. This is controlled by Facet API. Unfortunately there is not much documentation for Facet API, and the API can be difficult to understand.

This post will change that!

In this post, you will learn how to use FacetAPI to create powerful custom faceted searches in Drupal 7. I will take a look at the UI of the module as well as its internals, so that we can define our own widgets, filters, and dependencies using some cool ctools features.

Introduction

This is a site that we built recently. It uses facets from SearchAPI instead of Apachesolr, but the general principle is the same.

As we move forward, I plan to answer the following questions:

  • How can I set the sort order of facet links? How can I create my own sort order?
  • How can I show/hide some facets depending on custom conditions? For example, how can I show a facet only if there are more than 100 items in the search results
  • How can I exclude some of the links from the facets?
  • How can I change the facets from a list of links to form elements with autosubmit behavior?

I will start with showing the general settings that can be configured out of the box and then dive into the code to show how those settings can be extended.

Block settings options


Let's take a look at the contextual links of the facet block. We can see there are three special options:

  • Configure facet display
  • Configure facet dependencies
  • Configure facet filters

Let's take a closer look at each of these.

Facets UI

On the facet display configuration page we can choose the widget used to display the facet and its soft limit, the setting of the widget, which controls how many facets are displayed. We can also use different sort options to set the links to display in the order we prefer.

The next settings page is for Facet dependencies. Here we can choose different options that will make Drupal understand when to show or hide the facet block.

The third settings page is Facet filters. Here we can filter what facet items to show (or not show) in the block.

As you can see, there are plenty of options in the UI that we can use when we set up our facets. We can filter some options, change sort order, control visibility, and even change the facet widget to something completely different than a list of links.

Now let's take a look at the internals of Facet API and learn how we can define our own facet widgets, facet dependencies, and facet filters.

Facet API is built on the ctools plugins system, which is a very, very flexible instrument.

Build your own facet widget

Imagine we want to create a facet that will consist of a select list of options and a submit button. Better yet, let's hide the submit button and just autosubmit the form when we change the value by selecting an item from the list.

In order to define our own widget we need to implement the following hook:

<?php
/**
* Implements hook_facetapi_widgets()
*/
function example_facetapi_widgets() {
  return array(
   
'example_select' => array(
     
'handler' => array(
       
'label' => t('Select List'),
       
'class' => 'ExampleFacetapiWidgetSelect',
       
'query types' => array('term', 'date'),
      ),
    ),
  );
}
?>

With this we define a new widget called "example_select" which is bound to a class called "ExampleFacetapiWidgetSelect".

All our logic will be in this ExampleFacetapiWidgetSelect plugin class:

<?php
class ExampleFacetapiWidgetSelect extends FacetapiWidget {
  
/**
   * Renders the form.
   */
 
public function execute() {
   
$elements = &$this->build[$this->facet['field alias']];     $elements = drupal_get_form('example_facetapi_select', $elements);
  }
}
?>

Instead of rendering the links directly, we will load a form to show our select element.

Now let's see how to build the form:

<?php
/**
* Generate form for facet.
*/
function example_facetapi_select($form, &$form_state, $elements) {   // Build options from facet elements.
 
$options = array('' => t('- Select -'));
  foreach (
$elements as $element) {
    if (
$element['#active']) {
      continue;
    }
   
$options[serialize($element['#query'])] = $element['#markup'] . '(' . $element['#count'] . ')';
  }  
$form['select'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#attributes' => array('class' => array('ctools-auto-submit')),
   
'default_value' => '',
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Filter'),
   
'#attributes' => array('class' => array('ctools-use-ajax', 'ctools-auto-submit-click')),
  );  
// Lets add autosubmit js functionality from ctools.
 
$form['#attached']['js'][] = drupal_get_path('module', 'ctools') . '/js/auto-submit.js';
 
// Add javascript that hides Filter button.
 
$form['#attached']['js'][] = drupal_get_path('module', 'example') . '/js/example-hide-submit.js';   $form['#attributes']['class'][] = 'example-select-facet';   return $form;
}
/**
* Submit handler for facet form.
*/
function example_facetapi_select_submit($form, &$form_state) {
 
$form_state['redirect'] = array($_GET['q'], array('query' => unserialize($form_state['values']['select'])));
}
?>

In this form, the value of each select box option is the url where the page should be redirected. So, on the submit handler we simply redirect the user to the proper page. We add autosubmit functionality via ctools' auto-submit javascript. Also, we add our own javascript example-hide-submit to hide the Filter button. This enables our facet to work even if javascript is disabled. In such a case, the user will just need to manually submit the form.

Each element that we retrieved from FacetAPI has the following properties:

  • #active - Determines if this facet link is active
  • #query - The url to the page that represents the query when this facet is active
  • #markup - The text of the facet
  • #count - The number of search results that will be returned when this facet is active.

Please note how easy it is to add the autosubmit functionality via ctools. It is just a matter of setting up the right attributes class on the form elements, where one is used as sender (.ctools-autosubmit) and the other one is the receiver (.ctools-autosubmit-click). Then we just need to add the javascript in order to get the autosubmit functionality working.

Please use minimum beta8 version of the FacetAPI module.

Sorting order

All options in facets are sorted. On the settings page above, we saw different kinds of sorts. We can also define our own type of sorting if none of the options on the settings page meet our needs. As an example, I will show you how to implement a random sort order.

To reach this goal we need to implement hook_facetapi_sort_info():

<?php
/**
* Implements hook_facetapi_sort_info().
*/
function example_facetapi_sort_info() {
 
$sorts = array();   $sorts['random'] = array(
   
'label' => t('Random'),
   
'callback' => 'example_facetapi_sort_random',
   
'description' => t('Random sorting.'),
   
'weight' => -50,
  );   return
$sorts;
}
/**
* Sort randomly.
*/
function example_facetapi_sort_random(array $a, array $b) {
  return
rand(-1, 1);
}
?>

The sort callback is passed to the uasort() function, so we need to return -1, 0, or 1. Note that you again get a facetapi element, which has the same properties as outlined above, so you could, for example, compare $a['#count'] and $b['#count'].

In order to see the result we just need to disable all the other sort order plugins and enable our own.

Filter

When facet items are generated they are passed on to filters. If we want to exclude some of the items, we should look at the filters settings. We can also, of course, implement our own filter.

As an example, we will create a filter where we can specify what items (by labels) we want to exclude on the settings page.

<?php
/**
* Implements hook_facetapi_filters().
*/
function example_facetapi_filters() {
  return array(
   
'exclude_items' => array(
     
'handler' => array(
       
'label' => t('Exclude specified items'),
       
'class' => 'ExampleFacetapiFilterExcludeItems',
       
'query types' => array('term', 'date'),
      ),
    ),
  );
}
?>

Like with the widgets, we define a filter plugin and bind it to the ExampleFacetapiFilterExcludeItems class.

Now lets take a look at the ExampleFacetapiFilterExcludeItems class:

<?php /**
* Plugin that filters active items.
*/
class ExampleFacetapiFilterExcludeItems extends FacetapiFilter {   /**
   * Filters facet items.
   */
 
public function execute(array $build) {
   
$exclude_string = $this->settings->settings['exclude'];
   
$exclude_array = explode(',', $exclude_string);
   
// Exclude item if its markup is one of excluded items.
   
$filtered_build = array();
    foreach (
$build as $key => $item) {
      if (
in_array($item['#markup'], $exclude_array)) {
        continue;
      }
     
$filtered_build[$key] = $item;
    }     return
$filtered_build;
  }  
/**
   * Adds settings to the filter form.
   */
 
public function settingsForm(&$form, &$form_state) {
   
$form['exclude'] = array(
     
'#title' => t('Exclude items'),
     
'#type' => 'textfield',
     
'#description' => t('Comma separated list of titles that should be excluded'),
     
'#default_value' => $this->settings->settings['exclude'],
    );
  }  
/**
   * Returns an array of default settings
   */
 
public function getDefaultSettings() {
    return array(
'exclude' => '');
  }
}
?>

In our example, we define settingsForm to hold information about what items we want to exclude. In the execute method we parse our settings value and remove the items we don't need.

Again please note how easy it is to enhance a plugin to expose settings in a form: All that is needed is to define the functions settingsForm and getDefaultSettings in the class.

Dependencies

In order to create our own dependencies we need to implement hook_facetapi_dependencies() and define our own class. This is very similar to the implementation of creating a custom filter, so I am not going to go into great detail here. The main idea of dependencies is that it allows you to show facet blocks based on a specific condition. The main difference between using dependencies and using context to control the visibility of these blocks is that facets whose dependencies are not matched are not even processed by FacetAPI.

For example, by default there is a Bundle dependency that will show a field facet only if we have selected the bundle that has the field attached. This is very handy, for example, in the situation when we have an electronics shop search. Facets related to monitor size should be shown only when the user is looking for monitors (when he has selected the bundle monitor in the bundle facet). We could create our own dependency to show some facet blocks only when a specific term of another facet is selected. There are many potential use cases here. For example you can think of a Facet that really is only interesting to users interested in content from a specific country. So you could process and show this facet only if this particular country is selected. A practical example would be showing the state field facet only when the country "United States" is selected as you know that for other countries filtering by the state field is not useful. Being able to tweak this yourself gives you endless possibilities!

Here is a shorted code excerpt from FacetAPI that can be used as a sample. It displays facet if the user has one of the selected roles. The main logic is in execute() method.

<?php
class FacetapiDependencyRole extends FacetapiDependency {   /**
   * Executes the dependency check.
   */
 
public function execute() {
    global
$user;
   
$roles = array_filter($this->settings['roles']);
    if (
$roles && !array_intersect_key($user->roles, $roles)) {
      return
FALSE;
    }
  }  
/**
   * Adds dependency settings to the form.
   */
 
public function settingsForm(&$form, &$form_state) {
   
$form[$this->id]['roles'] = array(
     
'#type' => 'checkboxes',
     
'#title' => t('Show block for specific roles'),
     
'#default_value' => $this->settings['roles'],
     
'#options' => array_map('check_plain', user_roles()),
     
'#description' => t('Show this facet only for the selected role(s). If you select no roles, the facet will be visible to all users.'),
    );
  }  
/**
   * Returns defaults for settings.
   */
 
public function getDefaultSettings() {
    return array(
     
'roles' => array(),
    );
  }
}
?>

Conclusion

As you can see, FacetAPI gives us the option to change anything we want about facets. We can change the display, alter the order, filter some facets out, and control facet blocks visibility and content based on dependencies we define.

I would like to thank the maintainers of this module, Chris Pliakas and Peter Wolanin, for their great work!

An example module is attached to this article. Thank you for reading, and if you have any further questions please let us know in the comments!

(Author: Yuriy Gerasimov, Co-Author: Fabian Franz, Editor: Stuart Broz)

AttachmentSize 1.99 KB
Nov 11 2011
Nov 11

With each week that DrupalCon Denver is coming nearer, the excitement in the community grows. At Trellon, we're not immune to that excitement, and we're proud to be a Platinum sponsor of Drupalcon Denver. We're looking forward to seeing members of the Drupal community there, both old friends and new. We're excited about the opportunity that Drupalcons give us not only to learn more about the direction of Drupal, but to help shape it. We're eager to learn what new and exciting things people around the world have been doing with Drupal. We want to hear about the great things you've built with Drupal, and we'd be happy to talk about what we've been up to lately, too.

We're also excited about sharing things that we've learned with you. As a result, we've put together proposals for a wide range of sessions.

Until midnight on Monday, November 14, you can head over to the Drupalcon site and vote for any of these that you'd like to attend.

We are already now looking forward to DrupalCon Denver to meet all of you great guys in person. Until Denver!

Oct 28 2011
Oct 28

Last week I attended LatinoWare 2011 in Foz do Iguaçu, Brazil, which also hosted, among a number of events, DrupalCamp Foz. There hasn't been a Drupalcamp in Brazil for some time already, so this was a really important event for the Drupal community in Brazil. The community is growing in Brazil and other Latin America countries. At the Drupalcamp, we had representatives from Peru, Argentina, Bolivia, and Mexico. We also had the presence of our dear Drupal creator, Dries Buytaert.


Wonderful Iguazu Falls - photo by Saulo Amui

I represented Trellon LLC, which was one of the Gold sponsors. Several other companies were also represented, contributing both with sponsorships and speakers. The community has also matured over time. The ecosystem now has all the key elements needed to succeed in increasing Drupal adoption in Latin America. We need, however, the support provided by the international community, such as the Drupal Association and global companies. To the latter, I would note that Brazil is a leading (and growing!) economy with a huge demand for powerful web solutions.


A bunch of Drupal nerds getting wet. Photo by Saulo Amui

Speaking on behalf of my fellow Latin American drupalists, we would like to expand the Drupal world out of the common North America-Europe axis. On the other hand, we would like to demonstrate this practically by first organizing a strong community here. There will be two scheduled events to foster this: Drupal Summit Guadalajara in Mexico and DrupalCon São Paulo in Brazil. Yes, we hope to bring the biggest Drupal convention to South America for the first time.

Oct 12 2011
Oct 12

CTools is a powerful toolbox for Drupal developers, but there are many features of this huge module that aren't well known or used. I would like to share some of my experience with using one of those lesser-known features: CTools Access Rulesets.

In this article you'll learn how to utilize the nice CTools Access Rulesets GUI in combination with some custom code to limit file downloads based on certain criteria. This promotes generic code and separates the access logic out to a place where it is customizable by a site administrator.

This gives you the ability to use flexible and powerful Panels selection rules outside of Panels.

Introduction

CTools Access Rulesets allow you to create access rules based on different contexts. It is widely used in page manager when we define rules for access to the page. When we enable the ctools_access_ruleset module, we use a UI to create our own access rules (admin/structure/ctools-rulesets/list).

One way we recently used this functionality was to limit private file downloads. I wanted to have a flexible rule that was customizible via UI, one that I would be able to extend if client's requirements changed (and I knew they would). To manage this, we wrote our own module and used hook_file_download to trigger our ctools access ruleset to determine whether we should allow the user to download the file or not.

We decided to base access control on taxonomy terms linked to the node that have the file attached. To do this we can create a ruleset that will have the context as nodes and there we check for the condition of whether or not the specified taxonomy terms are attached to these nodes.

Setting up the rule (in the GUI)

The screenshots below (click to enlarge) will allow us to see the UI interface to see the how to set it up.

First we need to open the list of access rulesets: admin/structure/ctools-rulesets/list

Each rule consists of three parts: basic infomation, contexts, and rules. Basic information is the name and description. I will skip this part.

For the Contexts we add the required context Node.

We also add the Relation to the taxonomy term we want to use. In my case, the term is from the Category vocabulary.


Now lets look at the settings of the Rules.


We have only one Rule based on a taxonomy term. The settings are as follows:


Creating the code

Now let's see how we can execute the ctools ruleset in our own module.

The key function is ctools_access($access, $context). $context is the array of contexts that we pass to the ruleset. $access is the configuration array. The machine name of my ruleset is document_download_rule.

<?php
 
// Prepare arguments for ctools_access().
  // Name of created ruleset is document_download_rule.
 
$access = array('plugins' => array(
    
0 => array(
      
'name' => 'ruleset:document_download_rule',
      
'settings' => NULL,
      
'context' => array('node_context'),
      
'not' => FALSE,
     ),
   ),
  
'logic' => 'and',
  
'type' => 'none',
  
'settings' => NULL,
  );  
ctools_include('context');
 
$node_context = ctools_context_create('entity:node', $node);
 
$contexts = array('node_context' => $node_context);   if (ctools_access($access, $contexts)) {
   
// Allow access.
 
}
  else {
   
// Deny access.
 
}
?>

$node is the fully loaded node object that we find based on the file $uri. I will skip this part as it is not too important here. However you can take a look at a full example that is attached to this post here: Download Example

If we will decide to extend the ruleset in the future, the only things we will need to do is add new rules and contexts via the UI.

In order to understand how to prepare the $access configuration array, I would recommend doing what I did: enable module page manager, play with access rules by creating rulesets, and debug the $access variable in ctools_access function.

Conclusion

By using CTools Access Rulesets we created a custom module, which allowed us to limit file downloads based on certain criteria. We showed that this method is a way to quickly write really flexible code that can adapt to changing needs.

It should give you a great perspective on what is possible using CTools Access Rulesets and stir your imagination. Have you used CTools Access Rulesets in a clever way in the past? Please share your experiences in the comments.

Thanks for reading!

AttachmentSize 38.52 KB 55.3 KB 50.13 KB 57.51 KB 53.14 KB 61.6 KB 1.06 KB
Sep 22 2011
Sep 22

In this article I would like to talk about creating low bandwidth versions of websites running on Drupal 6.x.

The times of dial-up and other types of slow internet connections are (almost) over, but sometimes it's still important to give your users the option to experience a light version of your website. I will explain how to implement such functionality using the context module. This will also serve as a good example of how to write context conditions.

The idea is pretty simple; If a user clicks on a 'Low bandwidth version' link, we hide some “heavy” blocks on the site. As the context module can hide and show blocks, we only need to implement a condition and use it to switch between versions of the site.

A technical aspect of this implementation is that we can keep information about the version of the site that the user requested via cookies, $_GET variable or HTTP headers (E.g. setting a header flag based on a cookie in a load balancer). This was suggested by my colleague Fabian Franz, who I would like to thank for this great idea.

The reason why we do not simply use $_SESSION to store the context is because we would like to use Varnish(reverse proxy web server). We want to keep anonymous users served from the varnish cache and reverse proxies do not support use of the $_SESSION variable. I.e. Pages will be generated by Drupal and served by Apache/lighttpd/nginx.

Let's start by writing the condition for the context module. The context module is extensible via plugins, so first we need to tell it where to look for our plugin.

<?php
/**
* Implementation of hook_context_plugins().
*/
function context_bandwidth_context_plugins() {
 
$plugins = array();   $plugins['context_bandwidth_condition'] = array(
   
'handler' => array(
     
'path' => drupal_get_path('module', 'context_bandwidth') . '/plugins',
     
'file' => 'context_bandwidth_condition.inc',
     
'class' => 'context_bandwidth_condition',
     
'parent' => 'context_condition',
    ),
  );   return
$plugins;
}
?>

Next, we need to register our plugin so that it is a condition plugin.

<?php
/**
* Implementation of hook_context_registry().
*/
function context_bandwidth_context_registry() {
  return array(
   
'conditions' => array(
     
'context_bandwidth_condition' => array(
       
'title' => t('Bandwidth'),
       
'description' => t('Used for low bandwidth version of the site.'),
       
'plugin' => 'context_bandwidth_condition',
      ),
    ),
  );
}
?>

Next, we define when we “execute the plugin”. This is the tricky part, but the idea is that during code execution, we need to check whether the condition of our plugin is met or not. Since we need to know what site version to show as early as possible, we execute on hook_init().

<?php
/*
* Implementation of hook_init().
*/
function context_bandwidth_init() {
 
// Set context.
 
if ($plugin = context_get_plugin('condition', 'context_bandwidth_condition')) {
   
$plugin->execute('all');
  }
}
?>

Now let's look at the plugin code. This is in the file includes/context_bandwidth_condition.inc.

The tricky part is that the plugin method condition_values() should always return something, otherwise the condition won't be saved.

In the options form we let the user choose when the condition will be met; I.e. Whether the site is showing the Low Bandwidth or High Bandwidth version. This will look like the following:

The last part is execution. We check what the settings of the condition are and run $this->condition_met($context) if the condition is satisfied.

<?php
/**
* Check whether current site should be Low bandwidth version.
*/
class context_bandwidth_condition extends context_condition {
  function
condition_values() {
    return array(
       
1 => t('Active condition')
    );
  }   function
options_form($context) {
   
$defaults = $this->fetch_from_context($context, 'options');
    return array(
     
'mode' => array(
       
'#title' => t('Active for'),
       
'#type' => 'select',
       
'#options' => array(
         
'low' => t('Low Bandwidth version of the site'),
         
'high' => t('High Bandwidth version of the site'),
        ),
       
'#default_value' => isset($defaults['mode']) ? $defaults['mode'] : 'low',
      ),
    );
  }   function
execute($value) {
    if (
$this->condition_used()) {
     
// Check if current site is Low Bandwidth.
     
$condition = context_bandwidth_is_lowbw_version();       foreach ($this->get_contexts() as $context) {
       
// Get the setting whether we should react on
        // Low or High bandwidth site version.
       
$options = $this->fetch_from_context($context, 'options');
       
$mode = !empty($options['mode']) ? $options['mode'] : 'low';         if (($condition && $mode == 'low') || (!$condition && $mode == 'high')) {
         
$this->condition_met($context);
        }
      }
    }
  }
}
?>

The function context_bandwidth_is_lowbw_version() checks the current version of the site in the following way:

<?php
/**
* Get current site version.
*
* @return boolean
*   TRUE if current site is Low Bandwidth version.
*/
function context_bandwidth_is_lowbw_version() {
  return (
         (isset(
$_COOKIE["low_bandwidth"]) && $_COOKIE["low_bandwidth"] == 1)
      || (isset(
$_GET["low_bandwidth"]) && $_GET["low_bandwidth"] == 1)
      || (isset(
$_SERVER["X-Use-LowBandwidth"]) && $_SERVER["X-Use-LowBandwidth"] == 1));
}
?>

Another remaining issue that we have to solve is how to implement the “switcher.” For this we will create a block.

In order to set the cookie we will use the jQuery cookie plugin (included in http://drupal.org/project/jquery_plugin). In our code we export the settings of the cookie.

<?php
/**
* Implementation of hook_block().
*/
function context_bandwidth_block($op = 'list', $delta = 0, $edit = array()) {
  switch (
$op) {
    case
'list':
     
$blocks['context_bandwidth_link'] = array(
       
'info' => t('Bandwidth version switcher'),
      );
      return
$blocks;
      break;
    case
'view':
     
$block = array();
      switch(
$delta) {
        case
'context_bandwidth_link':
         
$site_version = context_bandwidth_is_lowbw_version();           // Init cookies settings.
         
$cookie_path = ini_get('session.cookie_path');
         
$expires = ini_get('session.cookie_lifetime');
         
$expires = (!empty($expires) && is_numeric($expires)) ? time() + (int)$expires : 0;           $settings = array(
           
'cookie_info' => array(
             
'name' => 'low_bandwidth',
             
'value' => $site_version ? 0 : 1,
             
'options' => array(
               
'path' => $cookie_path,
               
'domain' => ini_get('session.cookie_domain'),
               
'secure' => false, //(ini_get('session.cookie_secure') == '1')
             
),
             
'expires' => $expires,
            )
          );          
// Add javascripts and js settings.
         
jquery_plugin_add('cookie');
         
drupal_add_js(drupal_get_path('module', 'context_bandwidth') . '/context_bandwidth.js');
         
drupal_add_js(array('context_bandwidth' => $settings), 'setting');           $link_title = $site_version ? t('High bandwidth') : t('Low bandwidth');           // Prepare settings for the link.
         
$query = $_GET;
          unset(
$query['q']);
         
$current_path = $_GET['q'];           unset($query['low_bandwidth']);
          if (!
$site_version) {
           
$query['low_bandwidth'] = 1;
          }          
$block = array(
           
'content' => theme('context_bandwidth_block_switcher', $link_title, $current_path, $query),
          );
          break;
      }
      return
$block;
  }
}
?>

The javascript shipped with our module just sets the cookie from settings when the switcher link is clicked. The method outlined above is also a great method to retrieve any cookies set via javascript from Drupal. This is useful for example when Varnish filters out the cookies to be set like described in our recent Boosted Varnish article.

An alternative to using cookies for saving the state, is using the Persistent URL module. In that case the url would not change for the normal site, but be prefixed with for example /light/ for the low bandwidth version of the site.

You are welcome to test the code of the module and give some feedback. I have attached the code at the bottom of this post (Download). I will greatly appreciate any comments.

Thanks for reading.

AttachmentSize 2.13 KB
Aug 25 2011
Aug 25

Note: I am hosting a BoF at DrupalCon London about this: Join us in Room 333 on Thursday 25th August from 11:00 - 12:00 (second half).

Introduction

Varnish is a fast, really fast reverse-proxy and a dream for every web developer. It transparently caches images, CSS / Javascript files and content pages, and delivers them blazingly fast without much CPU usage. On the other hand Apache - the most widely used webserver for providing web pages - can be a real bottleneck and take much from the CPU even for just serving static HTML pages.

So that sounds like the perfect solution for our old Drupal 6 site here, right? (Or our new Drupal 7 site.)

We just add Varnish and the site is fast (for anonymous users) ...

The perfect solution?

But if you do just that you'll be severely disappointed, because Varnish does not work with Drupal 6 out of the box and, even with Drupal 7, you can run into problems with contrib modules. You also need to install an extra Varnish Module and learn VCL. And if you have a module using $_SESSION for anonymous users, you have to debug this and find and fix it all, because if Varnish is seeing any cookie it will by default not cache the page. The reason for this is that Varnish can't know if the output is not different, which is actually true for the SESSION cookie in Drupal. (Logged in users see different content from logged out ones). That means that those pages are not cached at all and that is true for all pages on a stock (non pressflow) Drupal installation.

So Varnish is just for experts then? Okay, we go with just Boost then and forget about Varnish. Boost just takes a simple installation and some .htaccess changes to get up and running. And we'll just add more Apache machines to take the load. (10 machines should suffice - no?)

Not any longer! Worry no more: Here comes the ultimate drop-in Varnish configuration (based on the recent Lullabot configuration) that you can just add. With minimal changes, it'll work out of the box.

That means that if you have Boost running successfully and can change your Varnish configuration (and isntall varnish on some server), you can run Varnish, too.

How to Boost your site with Varnish

And here are the very simple steps to upgrade your site from Boost to Boosted Varnish.

1. Download Varnish configuration here: http://www.trellon.com/sites/default/files/boosted-varnish.vcl_.txt
2. Install and configure Boost (follow README.txt or see documentation on Boost project page)
3. Set Boost to aggressivly set its Boost cookie
4. Setup Apache to listen on port 8080
5. Setup Varnish to listen to port 80
6. Replace default.vcl with boosted-varnish.vcl

Now we need to tweak the configuration a little:

There is a field in Boost where you can configure pages that should not be boosted. We want to make sure those pages don't cache in Varnish either.

In Boost this will just be a list like:

user
user/*
my-special-page

In Varnish we have to translate this to a regexp. Find the line in the configuration to change it and do:

##### BOOST CONFIG: Change this to your needs
       # Boost rules from boost configuration
       if (!(req.url ~ "^/(user$|user/|my-special-page)")) {
         unset req.http.Cookie;
       }
##### END BOOST CONFIG: Change this to your needs

And thats it. Now Varnish will cache all boosted pages for at least one hour and work exactly like Boost - only much faster and much more scalable.

We had a site we worked on where we had a time of 4s for a page request under high load and brought this down to 0.17s.

The only caveat to be aware of here is that pages are cached for at least one hour, so there is an hour of delay until content appears for anonymous users. But this can be set to 5 min, too, and you'll still profit from the Varnish caching. In general this setting is similar to the Minimum Cache Lifetime setting found in Pressflow.

The code line to change in boosted-varnish.vcl is:

##### MINIMUM CACHE LIFETIME: Change this to your needs
    # Set how long Varnish will keep it
    set beresp.ttl = 1h;
##### END MINIMUM CACHE LIFETIME: Change this to your needs

Even 5 min of minimum caching time give tremendous scalability improvements.

Actually with this technique I can instantly make any site on the internet running Boost much much faster. I just set the backend to the IP, set the hostname in the VCL and my IP address will serve those pages. So you could even share one Varnish server instance for all of your pages and those of your friends, too. I did experiment with EC2 micro instances and it worked, but for any serious sites you should at least get a small one. I spare the details for another blog post though - if there is interest to explore this further.

How and Why it works

The idea of this configuration is quite simple.

Boost is a solution which works well with many many contrib modules out of the box. With Varnish you need to use Pressflow or Drupal 7 and you need to make sure no contrib modules are opening sessions needlessly, which can be quite a hassle to track down. (Checkout varnish_debug to make this task easier here: http://drupal.org/sandbox/Fabianx/1259074)

But Boost's behavior and rules can be emulated in Varnish, because if it is serving a static HTML page, it could serve also a static object out of the Varnish cache.

And the property that is distinguishing between boosted and non-boosted pages is the DRUPAL_UID cookie set by Boost.

The cookies (and such the anonymous SESSION) are removed whenever Boost would have been serving a static HTML page, which would mean that Drupal never got to see that Cookies in the first place, so we can safely remove them.

The second thing to prevent Drupal from needlessly creating session after session is a very simple rule:

If a SESSION was not sent to the webserver, do not send a SESSION to the client. If a SESSION was sent to the webserver, return the SESSION to the client. So SESSION cookies will only be set on pages that are excluded from caching in Varnish like the user/login pages or POST requests (E.g. forms). As Drupal has the pre-existing SESSION cookie, it does not need to create a new SESSION.

To summarize those rules in a logic scheme:

# Logic is:
#
# * Assume: A cookie is set (we add __varnish=1 to client request to make this always true)
# * If boosted URL -> unset cookie
# * If backend not healthy -> unset cookie
# * If graphic or CSS file -> unset cookie
#
# Backend response:
#
# * If no (SESSION) cookie was send in, don't allow a cookie to go out, because
#   this would overwrite the current SESSION.

Why Boost and Varnish?

Now the question that could come up is: If I have Varnish, why would I need Boost anymore?

Boost has some very advanced expiration characteristics, which can be used for creating a current permanent cache on disk of the pages on the site.

This can help pre-warm the varnish cache in case of a varnish restart. But as it turns out, you can use the stock .htaccess and boosted varnish will still work - as long as the DRUPAL_UID cookie is set. It might be possible as further work to just write a contrib module doing exactly that.

But Boost can also be really helpful in this special configuration as you can set your Varnish cache to a minimum lifetime of - for example - 10 min. And instead of Drupal being hit every 10 min, Apache is just happily serving the static HTML page Boost had created until it expires.

The advantage of that is:

If there are 1000 requests to your frontpage, Apache will just be hit once and then Varnish will serve this cached page to the 1000 clients. So instead of Apache having to serve 1000 page requests, it just have to serve one every 10 min. Multiply that with page assets like images and CSS and JS files and you get some big savings in traffic going to Apache.

Conclusion

Varnish is a great technology, but it has been difficult to configure and there are lots of caveats to think of (especially with Drupal 6). This blog post introduced a new technology called Boosted Varnish, which lets Varnish work with every page that is running the Boost Module by temporarily adding it to the active working set of varnish and fetching it frequently back from the permanent Boost cache on disk. The purpose is not for those that are already running high performance drupal sites with Mercury stack, but for those that are using Boost and want to make their site faster by adding Varnish in front of it without having to worry about Varnish specifics.

I created a sandbox project to create any issues related to the configuration on:

Have fun with the configuration and I am happy to hear from you or see you tomorrow at my BoF session at DrupalCon London!

AttachmentSize 10.74 KB
Aug 19 2011
Aug 19

Well, it is just about time for CiviCon. Even though I am not going, I want to relay a good CiviCRM war story.

Recently, we went through the process of upgrading a CiviCRM site (Drupal 6.x - CiviCRM 3.4). This can turn into a painful process if you aren't careful, and sometimes even when you are. Among the possible issues you may run into, there are some nasty errors that break the upgrade process when attempting the database upgrade script. I'm going to tell you how to get past them.

First attempt

The whole upgrade process is complicated and requires a lot of attention. First of all backup your databases. Do not skip over this step. If there is a bug such as the one that I have encountered, there will be no easy way to restore the database structure and/or data without a backup to revert to.

It's essential to follow the upgrade guide. However, on step 8, you might stumble on the foreign keys bug. First, I thought it was somehow a corruption in the database structure, so I tried to repair it. Still, the error caused the upgrade process to quit. After a lot of trial and error, going version by version (we were upgrading from CiviCRM 2.2), I realized it was actually the data that was corrupt.

If at first you don't succeed, try and try again!

Well, if you run into a foreign key error (actually it will be issued as a constraint violation), it is likely that the data in the tables is corrupt. I don't know exactly how this could happen, but the fix depends on the situation. You will have to check the tables and look for the foreign key from the error message. This should be pointing to another table (hence, a foreign key), and some of it's values may be pointing to missing entries on the target table. Once you figure out the tables and fields properly, it's time to restore your backup and follow these steps:

  1. Run a manual fix on mysql to change all foreign keys that are pointing to missing entries. As an example, here is the query I had to run:
    UPDATE `civicrm_pcp_block` SET `supporter_profile_id` = NULL WHERE `supporter_profile_id` NOT IN (SELECT id FROM civicrm_uf_group);
    Adapt this query to your own needs. The 'supporter_profile_id' is the foreign key pointing to the 'id' field of table 'civicrm_uf_group'. You should test it on the broken database before.
  2. Perform system checks. I had some settings related file directories reverted. If anything looks weird, time to debug and maybe revert with the DB backup.
  3. Replace the codebase with the target version
  4. Run civicrm/upgrade

After that, you should be good to go with the upgrade. Carefully check your data, read the documentation thoroughly and enjoy the fruits of an upgraded CiviCRM installation.

Aug 02 2011
Aug 02

We would like to announce the creation of the Webform Bonus Pack module which allows you to do email routing and to send submissions digests in specific formats to a configured set of emails on cron.

This blog post aims to show different approaches of email routing (i.e.conditional emailing), to explain pros and cons of these approaches, and to show how to handle email routing efficiently using Webform Bonus Pack module.

Task statement

Let's assume we have a simple order webform for a Drupal webshop. It has four fields: Name, Email, What are you interested in, and Order details.

The What are you interested in field uses a multiple select component which have the following key-value pairs:

branding|Branding
graphic_design|Graphic design
web_design|Web design
website_support|Website support
website_performance|Website performance optimization
internet_marketing|Internet marketing
seo|Search engine optimization
hosting|Hosting

Assume we have following email routing rules:

Well known solutions

1. Webform's built in abilities

It is easy to send email to a custom address on every submission. Also we are able to send E-mail to an address which is coming from any of the webform components.

In our case we need to assign available values for the What are you interested in component to be like this:

[email protected]|Graphic design
[email protected]|Web design
[email protected]|Website performance optimization
[email protected]|Search engine optimization

As you can see it is easy to configure simple routing, however there are some limitations:

  • The key should be unique, hence it is impossible to use same email address for two or more keys.
  • It is impossible to send emails to multiple email addresses if a single item is selected.
  • Email addresses will be used instead of proper keys when viewing submission or downloading webform results.

As we can see, this is a fast solution, but it is not very configurable.

The Webform PHP module allows you to create a PHP component, which could be used to assign E-mail addresses to values (or whatever you want). In a PHP component you need to add specific PHP code that makes custom routing. Obviously in code you could read data from other components and use complex conditions. Although this is a highly configurable solution, it is very dangerous, so the module maintainer recommends against using this module unless the developer knows what he is doing.

3. Custom module

Using custom PHP code in your module for Webform validation or submission is more safe, and is also highly configurable, but it is more difficult to update routing rules and cannot be done easily by non-technical people.

Email routing using Webform Bonus Pack module

The Webform Bonus Pack module provides a mapping component, which can be easily used for email routing. Configuration is easy: you need to select the What are you interested in component as a mapped component and then add mapping key-pair values. Keys are the mapped component (e.g. What are you interested in) keys and values are emails. When items of the What are you interested in component are selected our routing component will return appropriate set of emails.

You need to select newly created Routing component as the E-Mail to address. And also don't forget to add [email protected] as custom email address to E-mails setting form.

Now you can test how it works: When you are submitting the form select the check-boxes for Website performance optimization and Hosting. Emails should then be sent to [email protected], [email protected], [email protected] and [email protected].

What's next?

I think that the mapping component will be evolving in the future. It would be good to have some functionality of or integration with the Rules module. Also, of course, I will be porting this to Drupal 7. In the next blog post about Webform Bonus module I am going to talk about another interesting feature, a submodule called Webform Digest, which already has integration with the mapping component.

May 18 2011
May 18

Facebook continues to grow at a rapid pace, and many sites have started to integrate Facebook Connect as a single sign on solution. Drupal has two modules available for integration with Facebook Connect: FB Module and Facebook Connect Module. This post uses our recent experiences to show how Facebook Connect can be extended in Drupal to offer a one-click to full profile (including image) solution. The results can look like this:

Stock functionality

As mentioned, there are two relevant modules here:

  • FB Module can be used for single sign on and complete Facebook application building, but it is quite complex.
  • Facebook Connect Module, on the other hand, only has the connect functionality and allows some access to the Facebook API, but it is simpler to use and extend.

Trellon's solution

While much of the functionality we needed was already available with the Facebook Connect Module, we needed a fully-integrated solution which would allow us to generate a full profile in one step. On the site we built, authenticated users can create acts of green, pledge to commit acts of green, create and register for events, and post comments. We wanted to eliminate barriers to participation which, in this case, meant creating an account quickly as part of a larger workflow.

Without Facebook integration, the user had to choose a password, supply a user name, and then upload his/her picture and fill out his/her profile information. This deterred many users. So, the site had many anonymous faces and lost quite a few potential users at the registration/login screens.

Our implementation of Facebook Connect changed this, and the many faces on the site prove this strategy to be a great success!

Now, when a user lands at the registration screen to pledge for an "Act of Green," s/he just needs to click "Quick-Register with Facebook," then allow the site access to his/her general information and email address. The user is then registered, logged in, and has pledged with his/her full name and user image.

This solution also increases site stickiness. When users return to the site and want to perform another action, they can easily login via facebook and be notified with a nice message telling them that they are already registered.

Can you make this any shorter?

Technical Challenges

The technical challenge here was to extend the facebook connect module to allow us to do all we wanted, while keeping our own customizations separate.

The first change we made to the module (which will soon be released as patches to the module) was to get destination= urls to work. Most of our workflow was dependent on this functionality.

<?php
 
if (user_is_anonymous())
  {
   
drupal_goto('act_commitment', array('act_id' => $node->nid))
  }
?>

and act_commitment was showing the drupal registration form.

Once this was complete, we had to create the possibilities to add "quick-register" buttons to the registration and login forms which are customized.

<?php
function mymodule_custom_add_fbconnect($type, $text, $desc, &$form)
{
 
$attr = array( "text" => $text );  if ($type == 'register') {
   
$value = fbconnect_render_register_button($attr);
  }
  else if (
$type == 'login') {
   
$value = fbconnect_render_login_button($attr);
  }
  else {
   
$value = fbconnect_render_button($attr);
  } 
$form['fbconnect_mymodule_button'] = array(
   
'#type' => 'item',
   
'#description' => $desc,
   
'#value' => $value,
   
'#weight' => -20,
   
'#id' => 'fbconnect_edn_button',
   
'#suffix' => '<hr />',
  );
}
?>

Those customizations could then be added as easy as:

<?php
   mymodule_custom_add_fbconnect
('register', t('Quick-Register via Facebook'), t('Register using Facebook'), $form);
?>

We also added custom hooks to allow for easier usage of the Facebook API by other modules and by supplying the data used for registration.

<?php
  $facebook
= fbconnect_get_facebook();
?>

Here we can now use $facebook object, which is giving us direct access to the Facebook SDK.

We also changed the quick registration to run the normal submit[] hooks to allow for usage of content profile and even made it possible to get the facebook data gained via quick registration:

<?php
function mymodule_custom_fbconnect_user_data($data) {
  global
$user$content_profile = content_profile_load('profile', $user->uid);  if ($content_profile->nid > 0) {
   
node_prepare($content_profile);
   
$content_profile->field_first_name[0]['value'] = $data['first_name'];
   
$content_profile->field_last_name[0]['value'] = $data['last_name'];
   
node_save($content_profile);
  }
}
?>

And last, but not least, we added some APIs to easily retrieve the image URL for display.

<?php
  $image_url
= fbconnect_get_user_image_url($user->fbuid, 'big');
?>

Conclusion and Future

All of those changes will be supplied as patches to the main fbconnect module in the following weeks. If you can't possibly wait any longer, you can download our fork below, which is attached to this post both as the module or as an archive of patches.

UPDATE: Since we originally posted this blog entry, some users reported issues with being able to log in using Facebook. We found an issue with how Varnish was configured that prevented some cookies from Facebook from properly being seen within Drupal. This has been resolved, and we adjusted our patches to prevent this error from happening again. The issue can be found here: http://drupal.org/node/1162960.

We also put up a demo site at http://fbconnect-demo.trellon.com/user/register that shows off the single click functionality. This site is not configured with Varnish, so it should be able to work for everyone regardless of how the server is configured.

AttachmentSize 58.27 KB 10.29 KB

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