Drupal and Node.js

I’ve been working on a project that required real-time Javascript updates based on user actions (including other users’ actions). At first the only solution I could see was polling the server every second via Ajax, but that (a) was very resource-intensive, (b) didn’t give me as much freedom with the JS as I wanted (though maybe that was my relative lack of experience with jQuery), and (c) though pretty close, wasn’t exactly real-time. I did my best to reduce the computations on the server as well as the bandwidth, but the result was still not that good. I had to find another solution, and I did: Node.js.

I’ve been working on a project that required real-time Javascript updates based on user actions (including other users’ actions). At first the only solution I could see was polling the server every second via Ajax, but that (a) was very resource-intensive, (b) didn’t give me as much freedom with the JS as I wanted (though maybe that was my relative lack of experience with jQuery), and (c) though pretty close, wasn’t exactly real-time. I did my best to reduce the computations on the server as well as the bandwidth, but the result was still not that good. I had to find another solution, and I did: Node.js.

As it happened, there is a Node integration module, but it still meant I had to set up a Node server somewhere. My localhost seemed like the logical place to start, especially since a Mac installer was available to download. The installation process wasn’t too hard; it did force me to dig into the command-liney parts of my Mac, which was a new experience. Likewise, I gave myself a crash course on Apache virtual hosts and iptables and a few related tasks. Problem, I could never get it to communicate with my local Drupal install, and I’m still not sure what I was doing wrong. In hindsight I probably should have taken better notes.

Things went a lot better when I installed Node on the actual production server. Different system, so I had to find different instructions: here for the Node server, and here for the Drupal side of things. And it worked, pretty much right out of the box! I say “pretty much,” since the instructions are kind of incomplete, and in at least one case the automatic config generator was wrong, and I had to tweak the config file manually. I’ll be blogging more about that, as well as raising the issue with the node.js project. As soon as I double-check that it really is an issue.

So yeah, we do live and learn. I’m glad this project pushed me out of my comfort zone and made me learn Linux development… not to say I’d ever do it full-time, but hey, knowledge is never a bad thing.

Argument 2 passed to db_delete() must be an array, string given

Sometimes it’s the stupid little things that trip you up. Just recently, I encountered an odd problem with db_delete() which I couldn’t figure out at the time; and, in fact, kept barking up the wrong tree until I found a proper example to work from. Even then I didn’t realise what I’d been doing wrong until yesterday.

Sometimes it’s the stupid little things that trip you up. Just recently, I encountered an odd problem with db_delete() which I couldn’t figure out at the time; and, in fact, kept barking up the wrong tree until I found a proper example to work from. Even then I didn’t realise what I’d been doing wrong until yesterday.

Very simply, I was calling db_delete() with an IN condition, like so:

$mytable_res = db_select('my_table','mt')
  ->distinct()
  ->fields('mt',array('key'))
  ->execute();

$key_arr = array();
foreach($mytable_res as $row) {
  $key_arr[] = $row->key;
}

db_delete('my_other_table','mot')
    ->condition('iid', $key_arr, 'IN')
    ->execute();

The first thing you’ll notice is that I should have been using db_query->fetchCol() to get the array, instead of tediously building it via PHP. I didn’t twig on that until later, because running that db_delete gave me an error: Argument 2 passed to db_delete() must be an array, string given

What did that mean? Googling didn’t really help. It was either something so obvious it didn’t bear mentioning, or something so obscure no one was talking about it. After a bit of thought I found an example to work from in the Drupal code base; specifically, in function aggregator_aggregator_remove modules/aggregator/aggregator.processor.inc:

$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchCol();
if ($iids) {
  db_delete('aggregator_category_item')
    ->condition('iid', $iids, 'IN')
    ->execute();
}

I adapted that to my own tables and fields (using fetchCol(), too), and it worked fine, but I still didn’t understand why my first attempt didn’t work. Finally, after some trial and error, I found the answer: db_delete doesn’t take a table alias. I was so used to using them in select functions that I automatically added one this time, and then kept on having a blind spot about it.

So, yeah. I wasn’t sure if I wanted to blog about this, but hey, we all make stupid mistakes, right? Might as well own it, learn, and move on.

Programmatically attaching image files to nodes in Drupal 7

I found the answer on Stackoverflow.

Simple, right? And this works fine if you’re attaching a brand new image.

I found the answer on Stackoverflow

I found the answer on Stackoverflow.

Simple, right? And this works fine if you’re attaching a brand new image. However, if you’re updating an image you’ll find that the styles don’t update after calling node_save(), and the image will not show up correctly on your site. You need one extra step: call image_path_flush() to regenerate that image’s styles before calling node_save().

//if the image is being updated, flush all its styles
image_path_flush($node->field_image['und'][0]->uri);

Ubercart 3: Programmatically submitting an order from Canada

I’ve been working on a project that, among other things, involves purchasing a particular item with one click: the order is created, payment information is added (how it’s stored is not important here) and the order is submitted, all automatically.

I’ve been working on a project that, among other things, involves purchasing a particular item with one click: the order is created, payment information is added (how it’s stored is not important here) and the order is submitted, all automatically. After a bit of flailing about on my own, I found this post on the Ubercart forums explaining how it’s done, and it totally saved my bacon. In short:

  1. add item to cart using uc_cart_add_item()
  2. populate and submit form ‘uc_cart_checkout_form’
  3. if things are okay, populate and submit form ‘uc_cart_checkout_review_form’ (which actually submits the order)

However, it’s not quite complete. 3 things are missing, in fact, which took me a bit of experimentation to figure out:

1) You need to enter the credit card info in the form_state array, not just in $_POST

The post says to do this:

// Define the credit card information in the $_POST directly.
  // See uc_payment_method_credit($op = 'cart-process') for why this is necessary.
 

$_POST['cc_number']    = 'XXXXXXXXXXXXXXXX';
  $_POST['cc_exp_month'] = 'XX';
  $_POST['cc_exp_year']  = 'XXXX';
  $_POST['cc_cvv']       = 'XXX';

But that didn’t work for me. It looks like you also need to enter it in the form_state, like so

$uc_cart_checkout_form_state['values']['panes']['payment']['details']['cc_number'] = 'XXXXXXXXXXXXXXXX';
	$uc_cart_checkout_form_state['values']['panes']['payment']['details']['cc_exp_month'] = 'XX';
	$uc_cart_checkout_form_state['values']['panes']['payment']['details']['cc_exp_year'] = 'XXXX';
	$uc_cart_checkout_form_state['values']['panes']['payment']['details']['cc_cvv'] = 'XXX';

2) Entering an address not in the store default country

(This post title says “Canada” because the default default store country is the US and that’s what I was testing with.)

Here’s the problem: if you try to enter a Canadian province in the $uc_cart_checkout_form_state['values']['panes']['billing']['billing_zone'] field, the ‘uc_cart_checkout_form’ form submission will fail with the following error message:

An illegal choice has been detected. Please contact the site administrator

Why? Because when the form is built, the allowed Zone values are based on the selected country. If all you can select are US States but you enter “BC” anyway, Drupal will not accept it, and that’s what the error message means.

Here’s the solution: the billing / shipping address panes are built based on the order country, and by default uc_order_new() creates an order with the billing and shipping countries set to the store default. So, all you need to do is set that field to a different value:

//after $order = uc_order_new()
$order->billing_country = 124; // Canada, or same value entered in $uc_cart_checkout_form_state['values']['panes']['billing']['billing_country']
//do the same for shipping country

This will populate the dropdown with Canadian provinces, and allow you to enter a province in $uc_cart_checkout_form_state['values']['panes']['billing']['billing_zone']

3) If you’re not starting from the cart, you need to change the redirection checks

The code checks if drupal_form_submit('uc_cart_checkout_form', $uc_cart_checkout_form_state) finished correctly like this:

if ($uc_cart_checkout_form_state['redirect'] == 'cart/checkout/review') {
    // We're good to move forward!!

And similarly for drupal_form_submit('uc_cart_checkout_review_form', $uc_cart_checkout_review_form_state)

Function uc_cart_checkout_form_submit($form, &$form_state) (defined in uc_cart/uc_cart.pages.inc) sets the form_state redirect:

function uc_cart_checkout_form_submit($form, &$form_state) {
  if ($form_state['checkout_valid'] === FALSE) {
    $url = $form_state['storage']['base_path'] . '/checkout';
  }
  else {
    $url = $form_state['storage']['base_path'] . '/checkout/review';
    $_SESSION['uc_checkout'][$form_state['storage']['order']->order_id]['do_review'] = TRUE;
  }

  unset($form_state['checkout_valid']);

  $form_state['redirect'] = $url;
}

If you’re starting from the cart, then $form_state['storage']['base_path'] will be equal to ‘cart’. But if your goal is a 1-click purchase, chances are you’ll be starting on a completely different page, and the redirect will be different as well. So you’ve got a couple of options:

  • Change $form_state['storage']['base_path'] somehow (is that possible?)
  • If you know the path of the page you started on, use it in the check:
    if ($uc_cart_checkout_form_state['redirect'] == 'yourpath/checkout/review') {
        // We're good to move forward!!
    
  • Be a little more flexible, and only check the last part:
    if (strpos($uc_cart_checkout_form_state['redirect'],'checkout/review') !== FALSE) {
        // We're good to move forward!!
    

db_query() and the IN() operator

A little while ago I was porting a Drupal 6 module to Drupal 7, which meant changing over a lot of queries. The new database API had oodles of new features and somewhat different rules, and I wanted to do things by the book.

Haven’t done a Drupal post in a while!

A little while ago I was porting a Drupal 6 module to Drupal 7, which meant changing over a lot of queries. The new database API had oodles of new features and somewhat different rules, and I wanted to do things by the book. Now, I thought I could leave the simpler queries to be static (ie: using db_query()) instead of dynamic (using db_select()) since db_select() apparently creates a lot of overhead… but a few just wouldn’t work. They were the ones using the IN() operator.

Here’s what my original code looked like:

$res = db_query('SELECT field FROM {table} WHERE cond_field in(@values)',array('@values' => implode(',',$array_of_ints));

My first stab in Drupal 7 was simply to replace the ‘@’ with a ‘:’

$res = db_query('SELECT field FROM {table} WHERE cond_field in(:values)',array(':values' => implode(',',$array_of_ints));

Didn’t work. There was no error, but the query didn’t return any rows. However, it did work when I plunked the imploded array right in the query string, like so:

$res = db_query('SELECT field FROM {table} WHERE cond_field in(' . implode(',',$array_of_ints) . ')');

Not pretty. Not clean. There had to be a better way, but what was the problem? And then it hit me: in Drupal 7 you don’t need to add single quotes around placeholders in the query, even if they represent strings, the API does all that for you if the value isn’t a number. So what was happening was that the value of the IN() clause was taken as one big string, which just happened to consist of comma-separated numbers.

What to do about it? I had to use a dynamic query:

$res = db_select('table')
  ->fields('table',array('field'))
  ->condition('cond_field',$array_of_ints,'IN')
  ->execute();

So there you go. It’s more involved, for sure, and note that the condition takes an array instead of a string, but it works. And it was a good introduction to dynamic queries, which used to scare me a little but now totally don’t. I’ve done far more complicated ones, with merges and subqueries and all sorts of crap, and db_select() can handle it all.

I was wrong and it feels good

So I’m putting the finishing touches on an UberCart 2 module, and I’m thinking it’s as good as it’s going to get. Then I go ahead and test it—the most visible test would be the creation of a custom node type and product class—and… no go. The class is created, but the node type isn’t.

So I’m putting the finishing touches on an UberCart 2 module, and I’m thinking it’s as good as it’s going to get. Then I go ahead and test it—the most visible test would be the creation of a custom node type and product class—and… no go. The class is created, but the node type isn’t.

After a bit of googling, I realised I’d been doing it the wrong way. My code just had straight db_query‘s to insert the records in {node_type} and {uc_product_classes}. Well, the latter may have been okay, but the former totally wasn’t. db_query does execute correctly, but at some later point the node_type is deleted. Unfortunately I can’t find the particular page that told me this anymore.

Using node_type_save() won’t help, either, since that just calls INSERT or UPDATE commands.

I’ve got options, though. Implementing hook_node_info() and hook_form(), for one, as detailed here. This means I don’t have the option of leaving the node type in the system until the module is uninstalled; also, it still doesn’t answer the question of creating a corresponding product class. I tried adding code to my hook_node_type() implementation to insert the class, but that just had the effect of not creating the node type while creating the product class. Weird.

Still, I’m feeling very positive. This isn’t the first bump I’ve encountered on my Drupal learning curve. Won’t be the last, either, as just today I started porting this module to UberCart 3 / Drupal 7. About time, too, I’ve been itching to dig into D7!

Adding custom fields to orders in Ubercart

Here’s a little something I figured out just recently. Let’s say you have Ubercart installed. Let’s say also that you want to add a custom field to the uc_orders table. There are a couple of ways to do this.

Here’s a little something I figured out just recently. Let’s say you have Ubercart installed. Let’s say also that you want to add a custom field to the uc_orders table. There are a couple of ways to do this.

One is to hack the uc_orders.install file’ and add the field to its schema definition. That seems untidy, and of course not upgrade-safe. Not to mention it won’t work if Ubercart is already installed. Wouldn’t you need to uninstall/disable uc_orders, drop the orders table, then reinstall it with the new scheme?

THere’s a better way, though. What I’ve done is write a separate module. Call it “mymodule,” just for originality. In mymodule.install, use db_add_field() like so:

mymodule_enable() {
  $ret = array();
  db_add_field($ret, 'uc_orders', 'newfield', array(
    'type'=>'varchar', 
    'length'=>50)
  );
}

Simple! So now I’ve added a field to the database. Calling uc_order_load() works fine: the new fields are part of the order object since it just reads from the orders table; however, calling uc_order_save() will not update the new field.

There’s one more thing I needed to do. uc_order_save() calls drupal_write_record(), which depends on the schema. So mymodule needs to add its new field to the schema, using hook_schema_alter():

So what you do is write the following in mymodule.module:

mymodule_schema_alter(&$schema) {
  $schema['uc_orders']['fields']['newfield'] = array(
    'type' => 'varchar',
    'length' => 50,
    'description' => 'custom field',
  );
}

And it works! I’m wondering if I could put this code in the .install file instead, in mymodule_schema(). Would that work? None of the documentation I’ve read says anything pro or con.

Looking Back at 2011

Not a big list this time, I’ll just mention two memorable highlights of 2011: I started working with Drupal, and I turned 40.

Not a big list this time, I’ll just mention two memorable highlights of 2011: I started working with Drupal, and I turned 40.

Drupal

Since October 2010 I’ve been in a long-term contract as a developer with a small web shop, building e-commerce sites. After a few months working in Magento I started on a Drupal project (specifically Ubercart), and instantly fell in love. Most of the work I’ve done in 2011 has been in Drupal, mostly back-end coding around Ubercart, though there’s been some digging into core code, as well as front-end / theme development here and there.

In the last year or so I’ve gained tons of experience in Drupal—as well as WordPress in a few smaller projects, both paid and volunteer. Working with both systems has been extremely enjoyable, and I feel I’ve finally found my niche. I know it’ll open many doors for me.

The Big 4-0

I admit, I used to be antsy about turning 40, but when it actually happened, I felt just fine. 40 is just a number, right? Plus, what with volleyball, Taiji, cardio and weights, I’m in way better shape at 40 than at 30—or, hell, even at 39. Finally, everyone The other part is everybody telling me I look way younger than 40. The general consensus is early-thirtyish, though I did have one friend ask me if I was 26 when I mentioned my birthday. And I was all o_O. Just goes to show: 40 is the new 30.

Content type descriptions in node/add

Most of my Drupal experience in the last year has been either purely back-end development, or superficial front-end stuff: JavaScript, AJAX, tweaking templates and styles, that sort of thing. But in the last couple of weeks, in two different projects, I’ve been digging more into theme development, menu management, and various other issues.

Most of my Drupal experience in the last year has been either purely back-end development, or superficial front-end stuff: JavaScript, AJAX, tweaking templates and styles, that sort of thing. But in the last couple of weeks, in two different small-scale projects, I’ve been digging more into theme development, menu management, and various other issues site admin issues. It’s fascinating stuff, though hard to wrap my head around. Plain old development on the Drupal platform was a lot more straightforward, at least to me. Still, it’s good to get out of my comfort zone, right?

Here’s one particular problem I dealt with, which I had a hell of a time googling for: the client wanted to change the content type descriptions on the “Add Content” (/node/add). It took me some trial and error and googling to find out that’s taken from two places:

  1. The content type description, as set in each Content Type.
  2. Overriding that if they exist, the descriptions of the “Add Content” menu item’s children.

There are still a lot of things I’m figuring out, like how to change the text on top of the Add Content page. Seems a simple thing, right? Maybe it is if you know how, but I don’t. It’s definitely not the menu system, though interestingly the menu item description does display as expected on its parent menu page…

How to disable individual fields in the node edit screen

Consider this problem: you want to disable a few fields on the node edit screen for particular node types. Let’s say the body, although this could apply to any and all fields. The reason for this is that for our client’s e-commerce system, the products are synched from an external POS system, and most fields should not be touched AT ALL… and certainly shouldn’t be overwritten with content copy-pasted from MS Word, which has happened a few times and prompted us to take preventative measures.

Consider this problem: you want to disable a few fields on the node edit screen for particular node types. Let’s say the body, although this could apply to any and all fields. The reason for this is that for our client’s e-commerce system, the products are synched from an external POS system, and most fields should not be touched AT ALL… and certainly shouldn’t be overwritten with content copy-pasted from MS Word, which has happened a few times and prompted us to take preventative measures.

I should clarify that at least one field (images) does still need to be editable, which is why we’re not just taking away edit permissions altogether.

Okay, so I’m not saying what follows is the best solution, just what I came up with, inspired by a Stack Overflow thread I can’t seem to find right now. It uses hook_form_alter() to (a) hide the body edit field group, and (b) replace it with static markup displaying the body content. I thought about replacing it with a disabled textarea, but in Firefox, for instance that’ll show up with a medium grey background that really doesn’t look very good.

$CONTENT_TYPE = 'product'; 
function MODULE_form_alter(&$form, $form_state, $form_id) {
  if(isset($form['#node']) && $form_id == $CONTENT_TYPE . '_node_form') {
    $form['displaybody'] = array(
      '#value' => '' . $form['body_field']['body']['#title'] . ':
' . $form['body_field']['body']['#default_value'] . '
', '#weight' => $form['body_field']['#weight'], ); $form['body_field']['#access'] = FALSE; } }

A few notes:

Line 3: I’m not really sure what the first condition is for. At first I thought it was to cover only editing existing nodes, but that doesn’t appear to be the case. The second condition is to cover only certain node types

Lines 4–10: we insert a new form element, ‘display body’. It will show the body field label and content, and will be have the same weight as the existing body field, so it shows up in the same position on the screen.

Line 11: This hides the body field, unconditionally.

But what if we wanted to allow some users to edit that field? That’s certainly doable, but it gets a bit awkward. I could use hook_perm() to create a special permission—call it ‘edit body’—and replace line 3 above with:

if(isset($form['#node']) && $form_id == $CONTENT_TYPE . '_node_form' && !user_access('edit body')) {

However, that permission would show up in my own module’s section, not with the other uc_product permissions, so that’s a bit annoying. I could hack uc_product.module, but that’s not really a good solution. If there were a way to add to a third party module’s permissions, that would be very awesome!