Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough
Dec 01 2010
Dec 01

I've been working with custom PHP MySQL apps for quite a few years. Early on I was building grant systems for nonprofit and State Arts Agencies in the United States. About 4 years ago, I fell into Drupal quite by accident at a little think tank that my then employer, The Western States Arts Federation, had organized in Vancouver. Several of the participants were employees of Bryght. I began to drink the cool aid. While at WESTAF I was responsible for the build of a couple of Drupal sites and found myself more and more drawn to the modular LEGO-like way you could snap pieces together to create something new. It was just about at that point that I starting going to Drupal meetups and transitioned to a Web Producer position with a Drupal shop. This was the beginning of my seeing Drupal as my bread and butter.

Fast Forward to November 2010. I've been to Drupalcons in Barcelona, Boston, Washington, Paris, Copenhagen, and San Francisco. I was involved in helping set up the original Drupalcamps in Colorado when you could count the participants on two hands and that has grown to several hundred. Austin was the first camp I attended outside of Colorado and I was impressed. Six out of the Seven Vintage Digital Co-op members attended.

I approach each of the meetups, cons, and camps as a place to potentially grow my understanding of Drupal as a profitable platform. They give me a chance to meet with leaders in our community, with other Drupal Professionals, and interact with different shops of different sizes. The sessions often validate and sometimes surprise me. In Austin, I attended nine sessions including Jeff Robbins' keynote. I wrote about each of the sessions on my personal blog. They are really just notes with video from the event - but if you have interest you can see them on dogstar.org tagged with drupalcamp austin. I was happily met with a few new community modules, mostly surrounding SEO, that I hadn't heard of before. I enjoyed the panel on Managing A Drupal Business. Panels like this allow people to validate what they think they know by hearing similar stories from other businesses.

I am seeing more and more of the office managers, accountants, content creators, marketers, and account managers at these events. This should be encouraged and more business track sessions ought to be solicited. As Drupal continues to mature and ease itself into the Enterprise we need to grow with it widening the pool of talents beyond project managers, developers, and themers. We need to thinking in strategic ways and leverage those who have traditionally not been within our community. All of what I'm seeing is encouraging.

It is easy to discount the social time at the camps and conferences. However, meeting people in a looser way at places like Austin's Dog and Duck Pub is also very important. These are the times that professional contacts are made, deals are often brokered, and jobs are offered. Conversations can be had that help you avoid technical and project oriented pitfalls that other developers have found themselves subject to. You might find out about an RFP that a friendly competitor isn't going to bid on because they are out of bandwidth.

Austin was an inexpensive way to feed the need for professional development and to continue to forge and support relationships. I, personally, am looking forward to more camps.

Jul 28 2010
Jul 28

(This is content originally posted at http://jcfiala.net/blog/2010/07/28/magic-command-invocation-drush.)

I've recently been digging into the Aegir Hosting System, both because we're starting to use it at my current NREL gig, and because I've proposed to do a session on it at DrupalCamp LA. In short, Aegir allows you to easily administer a number of Drupal sites from a common site, itself built on Drupal. It's really slick, and a lot of the functionality is built on Drush. (Also see http://drupal.org/project/drush .)

So I'm looking into how Aegir handles tasks, which are just what they sound like - nodes representing commands that Aegir fires off every minute or so from a queue - if the queue's empty then that's that, but otherwise it calls drush hosting-task #, where the # is a task node id. Wanting to know what that actually does, I looked around, found hosting.drush.inc, and discovered that it didn't have a callback.

Huh. I'd thought that Drush commands had to have a callback, but here I discover that they don't. So, I wondered, what's actually going on there? And, following the advice handed to me by Greg Knaddison when I started in Drupal, I started following the code. And here's what I found:

Generally, you want to use the callbacks on drush commands when they're fairly simple - the task is doing something basic that can be handled easily. If it's something more complex, you can skip the callback, and gain a fair amount of control over how your command is invoked, and even cleanup help if something goes south.

If a drush command doesn't have a callback, then the command info is handed off to a function called drush_invoke - you can find it in the command.inc file. (For the purposes of this, I'm going to call the command hosting-task, because that's what I was looking for when I found this out, and it's a good example.) The first thing drush_invoke does is look for an include file named after the reverse of the command name - so our hosting-task gets inverted and the dashes are converted to dots, leaving us with task.hosting.inc. It then looks around to find this file in any directory that contains a *.drush.inc file - the folks working on Aegir in this case put task.hosting.inc in the same directory as hosting.drush.inc, which makes sense. Now, there doesn't have to be an include file like this, and if Drush doesn't find one, it'll still continue to the next part. But if you've got a complex command that you're handing off to drush_invoke to handle for you, why not put it in it's own file for neatness' sake?

Continuing, after the possible file load, the drush command is turned into a base $hook by the simple expedience of changing any spaces or dashes into underscores - so for the example, we'd have a base hook of 'hosting_task' With that constructed, drush looks for functions named off of this command as follows:

  1. $hook_validate (ie, hosting_task_validate)
  2. pre_$hook (ie, pre_hosting_task)
  3. $hook (ie, hosting_task)
  4. post_$hook (ie, post_hosting_task)

And then, for each module that defines a module.drush.inc file, drush looks for a drush_$modulename_$function... which means that for the hosting module, it looks for:

  1. drush_hosting_hosting_task_validate
  2. drush_hosting_pre_hosting_task
  3. drush_hosting_hosting_task
  4. drush_hosting_post_hosting_task

If you look at task.hosting.inc, you'll see that most of these are defined - they're not actually using drush_hosting_pre_hosting_task, but that's because they're doing their preparation work in the drush_hosting_hosting_task_validate function. So, the drush_invoke happily continues along this series of commands, until it either reaches the end (oh, happiness), or one of these functions invokes drush_set_error() - in which case the whole stack of commands reverses and runs backwards, and drush_invoke sees if there's a function named exactly the same, only with '_rollback' appended to the name. (For our example, there is a drush_hosting_hosting_task_rollback() defined in task.hosting.inc.)

A note on naming - although above we've referenced drush_hosting_hosting_task_validate and drush_hosting_hosting_task, the folks who maintain Drush recognize that these are pretty long names to have to construct, and as such they check for when one of these function names they construct starts with drush_module_module and change it over to drush_module. So, in task.hosting.inc, they really should be naming the callback functions drush_hosting_task_validate and drush_hosting_task... but for now Drush recognizes the old form and still will call them.

So to summarize what I've said so far, if you run into a Drush command without a callback, what you really should be looking for is a file named the reverse of the name in the same directory as the hook_drush_command implementation, and that file should contain the drush_module_command functions. And alternately, if you want to create a Drush command that can rollback/clean up after itself, then you want to create your commands in a reverse-order name file.

But... there's something else.

drush_invoke goes through all of the modules which define drush hooks when looking for functions to call when invoking a drush command. So, if you wanted to do extra work on hosting-task drush commands, you could create an example.drush.inc in your example.module, and then define your own drush_example_hosting_task function. Heck, you could define any of:

  1. drush_example_hosting_task_validate
  2. drush_example_pre_hosting_task
  3. drush_example_hosting_task
  4. drush_example_post_hosting_task

This is pretty exciting, as it opens up a lot of customization not only in Drush, but also in Aegir. I think as Aegir continues to mature, we're going to start seeing a bunch of modules which extend it in various ways.

Mar 23 2010
Mar 23

The Idea

A number of months back, a group of us had the idea to create a software co-operative. There were several tenets that we decided to follow:

  • The company wouldn't have any employees -- everybody involved would be have 1099 status and would be an independent contractor
  • The company would be formed as an Limited Liability Company - we chose the state of Delaware
  • The company would seek to have a $0 cash and asset value at the end of each year
  • The company would be virtual to keep costs low
  • We would focus on working with open sourced projects like Drupal

The Setup

We set the company up using Instacorp. The benefit was the speed at which we could set up the company with an automatic legal presence in Delaware. The people at Instacorp made the process incredibly simple, asking a few questions. Within days the legal documents were delivered. That, in itself, really didn't make the company real.

After receiving the legal documents, it was necessary to obtain an FEIN for tax purposes. This is a simple process on the IRS site - it just takes a few minutes and you get the documentation electronically.

We needed to decide how to be taxed.
LLC's report taxes in one of 3 ways:

  • Disregarded entity (limited to one member LLC's)
  • Partnership (default if other elections are not made)
  • Corporation, electing to be taxes as pass-through entity, called S corporations.

In the case of Vintage Digital, workers are paid for what they do which is reported to them on FORM 1099 as commissions.
Great variation will occur in compensation since it is entirely based on hours worked and percentages for those who find clients and shepherd clients through the contracting process. The LLC is a virtual corporation, with exceeding low or non-existent overhead. There is no intent to use the LLC other than as a distribution method for sharing work; profits/losses will be kept to a minimum. There is no intent to hold fixed assets or incur debt.

In the final analysis both partnership and S-corporation reporting would be the same.

Our accountant indicated that the following things were recommended:

  • Use "S" corporation for tax reporting because the laws are better understood and simpler.
  • Majority of LLC's elect to report taxes as a pass-through corporations, hence even the IRS is more familiar with these tax laws.

We had an S-corp election to indicate how we were going to be taxed - after the election that document needs to be sent to the IRS.

We needed a bank account and opted for a bank that had free business checking and had online bill pay. The bank required our Articles and two forms of identification. We also needed a copy of our FEIN letter from the IRS.

The Tools
All companies need tools to help run things on a day to day basis. A virtual venture is no different. We needed management tools, communication tools, invoicing/book keeping software, and ways to manage contracts. To that end we sought out different solutions that would provide us with ways to sensibly manage ourselves and our projects.

  • Skype for Communication (both voice and chat)
  • Open Atrium for a client intranet and as an internal planning tool.
  • Bamboo Invoice for invoicing clients (although that might change as we transition to QuickBooks)
  • Drupal for our Web presence
  • dotProject for time record keeping and ticketing
  • Office for estimates, calculating commission shares, and contracts
  • Google Voice for incoming phone calls - the rest of us use our own mobiles

The Team

The team is comprised of:

All of us have (and continue to) contribute to the Drupal Project and are heavily involved in our local communities.

The team keeps in pretty much constant contact through Skype. We try to meet about once a month together to have Member/Board meetings. They have occurred in restaurants for brunch, member's homes, and also at a bowling alley - pretty much anywhere that is quiet and you can get through company issues. Fortunately for our crew, we all live with 30 miles or so of one another which makes getting together fairly easy.

Each project gets its own Skype room, project in Open Atrium, project in dotProject, and commission spreadsheet.

As clients come in, we assess who has bandwidth for a given project - the goal ensuring that each co-op member has enough work (in and outside of the co-op) to make a reasonable living. Co-op members are free to work as much or as little as they want (given the work is available). This arrangement was designed to give our team as much flexibility as possible.

Jan 07 2010
Jan 07

Recently I had the privilege of working on the conversion of Novus Biological's website to Drupal 6, selling scientific supplies using Ubercart. I did this as a freelancer for the talented folks at SpireMedia, working with them as well as with fellow Vintage Digital member Ben Jeavons. It was a fantastically intense experience.

Ubercart's a great system out of the box, but there were a number of features on the site which made this implementation difficult.

  1. There were about 70,000 products, which is probably two or three orders of magnitude greater than the usual Ubercart site.
  2. The site needed to work with three currencies, and four prices, depending on the user's location or selection. Ubercart doesn't handle this well.
  3. The site needed to have two kinds of discounts - ones available to most any user, and ones only available to users who had bought the discount with userpoints.

I'll probably write about all of these in time, but at the moment I want to comment on that second item.

A close-up of the novusbio website, showing the region-change text
If you take a look at www.novusbio.com, or the image above, you'll see a bit of text in the upper right hand corner which says something like 'US site', or 'Europe Site', or 'Great Britain site', or 'World Site', with a little arrow next to it letting you change your region. We would start by pre-selecting a region for the user based on which IP they were browsing from, and then users could change it. If you go to a product page on Novus' site, you'll see different prices based on which of these are currently selected, as well as different currency signs. Since doing something like this is what some people would like to be able to do, I wanted to take a little time to go over how we did this.

Note: If you're not a coder, and you need this functionality, then you're going to want to go find a coder to handle this for you. It's not currently possible to do by flipping a switch or installing a module. I'm going over the changes you need to make here, but the full set of changes, along with the needed patches to Ubercart, are contained in uc_example.zip, attached to this post.

To start with, you're going to need to define the regions or currencies you're doing prices for. I use the term regions, because you could well have two different areas of the world using the same symbol but different price amounts. So, I use a define to mark which regions I'm keeping track of.

define('UC_EXAMPLE_DOLLAR', 1);
define('UC_EXAMPLE_EURO', 2);
?>

There are basically two things you want to handle when you allow for more than one currency in Ubercart:

  1. You want to change the currency symbol.
  2. You want to change the numeric amount of the price. You probably won't want to have a factor that you apply to the base price - I would rather suggest setting up alternate prices as CCK fields on your product nodes, much the same way that books in the US have set US and Canadian prices printed on them.

(As an aside, although the changes I'm discussing will allow you to have, say, both US and Euro prices on your site, you'll also at some point need to go through and write up some new reports that are multi-currency aware. After all, a report that says you sold 54,382 in the last two months doesn't mean anything if 24,382 was in US Dollars and 30,000 was in Euros. And, if you do up some reports like this, why not share them with the rest of us?)

There's two different places where you need to keep track of the current currency type as well - for when a user is browsing the site, or putting together an order, or reviewing her cart, or whichever, you'll want to store the current currency type in the session. On the other hand, if you're viewing an order - either in the very last step before the order is committed, (where a row has been written to uc_orders) or when viewing an order in the history, you need to have stored the currency type in the order data itself.

Changing the amount/magnitude of the item

Now, the slightly simpler part is changing the amount of the charge - if you're looking at $360, then we're talking about the 360 part of it. This amount needs to change on cart items or on products when they're viewed. We don't need to worry about changing the amount on the items in completed orders, because that amount quite sensibly is fixed when the order is committed.

I'm not going to go over every change you need to make - you might need to do a little work in your views to decide which price to use when displaying a view (indeed, views_customfield may be a good idea for that), but when you're changing how the product is displayed, you're basically doing a little work with hook_nodeapi:

/**
* Implements hook_nodeapi().
*/
function uc_example_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (
$op == 'view') {
   
$region_price = uc_example_get_node_price($node, $_SESSION['region']);
   
   
$context['class'][1] = 'sell';
   
$context['field'] = 'sell_price';
   
$node->content['sell_price']['#value'] = theme('uc_product_price', $region_price, $context, array('label' => !$a3));
   
   
$context['class'][1] = 'display';
   
$context['field'] = 'sell_price';
   
$node->content['display_price']['#value'] = theme('uc_product_price', $region_price, $context);

  }
}

/**
* Returns the price from the node, from the region passed in.
*
* @param stdClass $node Product node to get price for.
* @param integer $current_region Region to get the price for.
*/
function uc_example_get_node_price($node, $current_region) {
  switch (
$current_region) {
    case
UC_EXAMPLE_DOLLAR:
      return
$node->field_price_us[0]['value'];
    case
UC_EXAMPLE_EURO:
      return
$node->field_price_euro[0]['value'];
    default:
      return
0;
  }
}
?>

When viewing the price in a person's cart, we'll want to use hook_cart_item($op, $item) to change the price for products.

/**
* Implements hook_cart_item()
*
* This lets us change the base price based on the user's current region.
*
* See http://www.ubercart.org/docs/api/hook_cart_item
* @param String $op     Operation being done
* @param stdClass $item Cart item - usually a node of some sort.
*/
function uc_example_cart_item($op, &$item) {

  </span>//dpm('hook_cart_item '. $op);
 
if ($op == 'load') {
    global
$user;
   
   
$product_node = node_load($item->nid);

    </span>//dpm($_SESSION);
   
$region_id = 0;
   
// If we're viewing the cart item on the review page, we want to use the region
    // that's embedded in the order item, not the region that the user is using.
   
if ($_GET['q'] == 'cart/checkout/review') {
     
$order = db_fetch_object(db_query(
       
"SELECT uo.order_id, IFNULL(uom.region, 1) as mc_region,
        IFNULL(uom.currency_sign, '$') AS mc_currency_sign
        FROM {uc_orders} uo
        INNER JOIN {uc_order_products} uop ON (uo.order_id = uop.order_id)
        LEFT OUTER JOIN {uc_order_multicurrency} uom ON (uo.order_id = uom.order_id)
        WHERE uid = %d and order_status = '%s' AND uop.nid = %d ORDER BY uo.order_id DESC"
,
       
$user->uid, 'in_checkout', $item->nid
     
));
      if (
$order) {
       
$current_region = $order->mc_region;
      }
      else {
       
$current_region = $_SESSION['region'];
      }
    }
    else {
     
$current_region = $_SESSION['region'];
    }

    if (</span>$product_node) {
     
$item->price = uc_example_get_node_price($product_node, $current_region);
    }
  }
}
?>

Changing the Currency Symbol

With that done, lets consider the currency sign. I like storing the user's currently selected symbol in $_SESSION['currency_sign'], and we'll also be storing it in orders as well. The first impulse you might have would be to start mucking about with variable_set('uc_currency_sign') - but changing that changes the symbol for everything at once, and you're no doubt hoping to have more than one customer at a time.

Instead you want to register a price handler with hook_uc_price_handler. Then, whenever a price is being determined by uc_price(), your price alteration function can determine the proper currency sign to use, and provide it.

/**
* All we're doing with this price alteration function, is telling it to use for the
* currency sign the sign that we've stashed in $_SESSION.
*/
function _uc_example_price_alter(&$price_info, $context, &$options) {
 
// default currency sign.
 
$options['sign'] = $_SESSION['currency_sign'];
  if (isset(
$context['subject']['order']) && $context['subject']['order']->order_id) {
   
$order = $context['subject']['order'];
    if (isset(
$order->mc_currency_sign)) {
     
$options['sign'] = $order->mc_currency_sign; // use sign from order!
   
}
    else {
     
$currency_sign = db_result(db_query("SELECT currency_sign FROM {uc_order_multicurrency} WHERE order_id = %d", $order->order_id));
      if (
$currency_sign) {
       
$options['sign'] = $currency_sign;
      }
    }
  }
}
?>

What I'm doing there is setting $options['sign'] - which is an array passed in by reference - and changing it to the sign we want it to have. First we set it by the $_SESSION, and then we check and see if the context - a collection of information about what this price is and where it's being presented - contains the order which this price is a part of. If it is a part of an order, then we use the sign saved as part of that order.

If you're wondering how this information gets to be part of the order, unsurprisingly the answer is we implement hook_order in our code.

/**
* Implements hook_order().
*
* This is where we finalize the region_id and currency_sign.
*/
function uc_example_order($op, &$arg1, $arg2) {

  if (</span>$op == 'new') {
   
// $arg1 is a reference to the order object.

    </span>$arg1->mc_currency_sign = $_SESSION['currency_sign'];
   
$arg1->mc_region = $_SESSION['region'];
  }
  if (
$op == 'save' && $_GET['q'] == 'cart/checkout') {
   
// Now we need to save our updated region data to the order!
   
if (db_result(db_query("SELECT count(*) FROM {uc_order_multicurrency} WHERE order_id = %d", $arg1->order_id))) {
     
db_query("UPDATE {uc_order_multicurrency} SET region = %d, currency_sign = '%s' WHERE order_id = %d",
              
$_SESSION['region'], $_SESSION['currency_sign'], $arg1->order_id);
    }
    else {
     
db_query("INSERT INTO {uc_order_multicurrency} (order_id, region, currency_sign) VALUES (%d, %d, '%s')",
              
$arg1->order_id, $_SESSION['region'], $_SESSION['currency_sign']);
    }
  }
  if (
$op == 'load') {
   
$multicurrency = db_fetch_array(db_query("SELECT region, currency_sign FROM {uc_order_multicurrency} WHERE order_id = %d",
                                            
$arg1->order_id));
   
$arg1->mc_region = $multicurrency['region'];
   
$arg1->mc_currency_sign = $multicurrency['currency_sign'];
  }
}
?>

And... then this is where we get to the unfortunate part. Remember that price handler above, where we pull the order information out of the context? Unfortunately, Ubercart is really inconsistent about providing us that needed information, even when the call to uc_price is happening in a function where $order is already just sitting there, all loaded up. So, I'd like to introduce these patch files, which are provided to you inside of uc_example.zip:

  • ubercart-621494.patch
  • ubercart-display-symbols.patch
  • ubercart-order-price.patch

These three patch files need to be applied to Ubercart, and force it to provide the $order as part of the $context when uc_price is being called. If you look through these files, you'll see that most of the time this $order object is already there - I'm just adding it to the $context['subject'] array. (Unfamiliar with patches? Copy the three files to the Ubercart directory, change your directory to that Ubercart directory, and then execute patch -p0 < {filename} for each one.)

Enclosed is a module that contains the changes I've mentioned above. It assumes that you've used cck to add a 'price_us' and 'price_euro' field to the product nodes, but otherwise you can use it as is, plug it into a test Ubercart installation, run, and try it out. The patches are only necessary when viewing a finalized order, particularly in the admin area, so you don't even need to use that when just experimenting with the code.

AttachmentSize uc_example.zip7.1 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