Blog

How to split Drupal Commerce orders at checkout

This week I needed to design a checkout workflow that, depending on the contents of a users shopping cart, would separate the current cart into two, and allow the user to check each out individually. Drupal Commerce comes with cart and checkout modules, to enable users to add products to a shopping cart, and then to checkout of your store. The commerce_cart module is really designed for a 1:1 user:cart ratio. That being said, it is possible to allow users to have more than one cart.

Commerce cart orders are standard orders with an order status that is recognized by the commerce_cart module as indicating that an order is in the shopping cart. All checkout pages provide an order status that is a cart status. For example the 'checkout', and 'review' checkout pages correspond to the checkout_checkout and checkout_review order statuses, which are recognized by commerce_cart as being cart orders. When selecting which order should be presented as the current shopping cart, commerce_cart looks for the first order, with a cart status, the greatest order_id. This is what is returned by commerce_cart_order_load().

In order to provide the required functionality I first provided a new checkout page and checkout pane, using hook_commerce_checkout_page_info() and hook_commerce_checkout_pane_info(). I placed the pane on the page and made the page the first in the commerce checkout workflow. I created a rule, using the Rules module, that would detect which types of items are in the cart, and depending on the business logic I was provided, would decide whether or not to show or hide the pane, and therefore the page as well. The rules were custom coded using some condition and action handlers that I created specifically for this project.

The checkout pane contains a simple form that allows the user to select which part of their order to checkout first. In the submit handler for the pane the line items that correspond with the order to be checked out first are removed from the current order, and added to a new cart order, created with comerce_cart_order_new(). Each order is then saved with the 'cart' status using commerce_order_status_update(), this is to make sure that some custom discount recalculation rules will fire for each order.

Finally I set the status of the new order to the original status of the current order (to reflect the current checkout page, and assign the new order object to $form_state['order'], this is so that the form will successfully redirect to the correct next checkout page for the new order. Once the customer completes the first order they are prompted, with an additional checkout pane on the 'complete' page of the checkout workflow, to begin the checkout process again with their remaining shopping cart.

I'm pretty happy with the current workflow and code, and am considering ways that it could be generalized and contributed back to the community.

Enabling SSL on MAMP Pro 2.0.5

Yesterday I needed to test a local copy of my client's site with SSL enabled. I'm running the site locally on a Macbook Air using MAMP Pro 2.0.5. There didn't appear to be any online instructions for enabling SSL in MAMP Pro, at least not in 2.0.5.

The user interface for MAMP Pro makes SSL options available, including an "SSL" checkbox on the "General" tab of the default host "localhost", and a dedicated "SSL" tab for each host, but this is all disabled for "localhost".

After experiencing a few roadblocks, including MAMP Pro locking up every time I loaded it, which I corrected by deleting the settings file in ~/library/Application Support/appsolute/MAMP PRO/settings.plist , I finally came up with a set of steps that work.

  1. Add a new host.
  2. Give the host a name (not "localhost")
  3. Set the port to 443 (make sure that you have set the Apache SSL port to 443 on Server > General
  4. Check the "SSL" checkbox
  5. Select the "SSL" tab for your new host and create a "Self signed certificate"
  6. Choose the key and certificate that you have created and "Apply" the changes

You can setup another host with the same name, and aliases if you have added any, but you * must * explicitly set the port on the non-SSL host to 80. I was never successful at getting SSL on the "localhost" host, even after setting the ports appropriately for both "localhost" instances I had in my list.

Looking at Acquia Dev Cloud and Pantheon

Until recently i was hosting my site on the AWS free-tier micro instance. This is a fully self-managed stack and I didn't mind this, it's nice to keep my system administration skills fresh (or a little less rusty). I've gotten busy recently, and wanted an opportunity to look at both Acquia Dev Cloud and Pantheon for Drupal hosting. The following is far less of a comparison than it is a brief synopsis of my experience with each.

The one thing I will say up front is that the pricing page for each service has serious accessibility problems. This raised flags for me initially. Neither of the services UIs is horrible for accessibility (from a screen-reader user's perspective) and other than what is mentioned below I didn't run into anything that I simply could not do on either service.

*** Spoiler alert ***

I chose Pantheon.

*** End spoiler alert ***

Acquia Dev Cloud

I was impressed initially with Acquia dev cloud's sign-up, that there is a free 30 day trial, with no credit card information required. If you're choosing the $99 / month Developer Acquia network subscription this also includes $44 off your first month with Drupalize.me, making it only $1. Along with the Acquia network subscription you need to choose a hardware (AWS EC2) configuration, I chose the minimum option, at $65 / month.

The dashboard was a bit daunting at first, but that is to be expected with a new environment. One annoyance that I experience was that the link to switch from svn to git was completely inaccessible to me using the JAWS 13 screen-reader with Firefox 9, I had to have someone else find and click it for me.

The experience was pretty smooth other than the fact that I had troubles getting a database dump. I don't know if there is a dashboard option for this, if so it isn't accessible. I tried to use drush sql-dump, and it gave errors. After professional and timely support from Acquia I was informed that the problem was reproducible. A little while later support got back to me and told me that it was that my version of drush 7.x-4.5 does not support remote sql-dump. After doing a git checkout master in my drush directory the remote sql-dump worked without a problem.

Pantheon

It was a little harder to get signed up with Pantheon than Acquia Dev Cloud. Sign up was free, but it required an invitation code. I had a code from back in August, but the account had expired and I needed to request another. It came pretty quickly, but this could have been because I had a prior code, your milage may vary.

I tried... and tried... and tried... to import my site into Pantheon using their dashboard / UI, it failed... and failed... and failed. After filing a support ticket and getting, what I would again consider to be, a timely response I was told that the UI doesn't always get to the end where the import wizard is, and I was provided a direct URL for this (hint: append /configure to your site's dashboard URL). *** Note creating a new site or importing a site through the /configure URL will destroy all data. Things were relatively smooth after this, and getting the second (client) site imported was a snap... or maybe a few snaps. The database import on the second site didn't seem to work the first time around, but it was possible to do this after the fact using the dashboard and using drush.

I decided to make my site 'live', which requires paying for hosting. I was presented with only one option, $50 / month for the first 6 months, this is half off the standard Professional rate of $100 / month. This is a great discount, but I really wanted the $25 Personal rate. I was told by someone who could see the pricing page that this is greyed out, i.e. not currently available. I bit the bullet and launched at the Professional rate.

The final step of making the site live was setting up the domain name. I added the domain names zufelt.ca and www.zufelt.ca to the 'live' section of my site's dashboard, and it told me to set the CNAME for each to edge.live.getpantheon.com. This seemed a bit tricky to me as I didn't believe it to be possible to set a CNAME on zufelt.ca, but I tried. I logged into my DNS service provider, dyn.com, and tried to set a CNAME for zufelt.ca, it wasn't possible. Apparently some DNS service providers believe that you shouldn't allow CNAME records on a second-level domain. Reading http://help.getpantheon.com/pantheon/topics/configuring_dns
I foundthat Google Apps Hosting will also fail if you use a CNAME on your second-level domain, luckily the page also provides the IP address for edge.live.getpantheon.com, with a commitment that users will be notified if the address changes in the future. It also states that Pantheon will be offering static IPs and managed DNS as add-ons in the future.

Conclusion

In the end I chose to go with Pantheon, I prefer their per-site pricing, and the fact that the dashboard UI was a bit more streamlined. I have nothing negative to say about Acquia Dev Cloud, it just wasn't the right solution for me.

When you lose your Amazon Web Services EC2 key pair

Ummm... So, a friend of mine, hypothetically speaking that is, might have, maybe, lost his or her Amazon Web Services Ec2 key pair. Okay, in reality I lost my key pair.

Like all of the other key pairs / passwords I use I figured that the next time I needed to access my instance I would visit the AWS Management Console and download a new key pair. No!

How to regenerate an AWS EC2 key pair for an EBS backed instance

  1. Shut down your instance
  2. Select Instance Actions for the instance and generate an AMI from the instance
  3. Launch a new instance and select the AMI you just created
  4. Select and download the new key pair
  5. Start your new instance and confirm that the new key pair works (on my Ubuntu instance the key is set to work with user ubuntu)
  6. Terminate your original instance

It didn't take to long and now my new instance is up and running with all of the old data. I now have backed up my private key somewhere that I hopefully will not lose it again.

Drupal Rules Domain access condition

Notice: This article does not advocate the use of PHP eval(), but it was the fastest way to get done what I needed :)

Yesterday I needed a condition to ensure that a rule executed on one particular domain of a site running Domain access. The Domain module doesn't seem to provide a Rules condition, and a super quick search of Google didn't find one either.

In order to solve my problem I could either write a custom module that makes a Rules condition available, using hook_rules_condition_info(), or use the Execute custom PHP code condition, I chose the easy path.

The value I used for the code in the condition was:

$domain = domain_get_domain();
if (!empty($domain['domain_id']) && $domain_['domain_id'] == 3) {
  return TRUE;
}
return FALSE;

Tags:

Drupal Rules components and recursion

This morning I created a Drupal Rules module rule set component, which is being triggered with a reaction rule. Some of the rules in the set have actions that will trigger the reaction rule, but recursion isn't happening. This is good, I don't want any recursion.

To be on the safe side I searched to find out why recursion isn't happening.

We can see that in rules_element_invoke_component() recursion is being intentionally prevented.

Tags:

Restricting a custom Drupal Rules action to execute once

Today I needed to apply some custom discounts to orders in Drupal Commerce. The Rules workflow is:

  1. Delete any existing discount line items from the order
  2. Calculate possible discounts and add new discount line items to the order
    1. I'm sure that there are other approaches, but this seemed simple (lol) and most importantly it seemed like it would satisfy my use case.

      The remove rule, and the calculate rules, all had the same event "After commerce order has been updated", with a condition that the order be a shopping cart order. This made sense, if someone updates their cart, then recalculate the discounts. The problem was that the rules were executed multiple times.

      Let's say that the user added an extra widget to their order, to which a custom discount applies.

      1. Add a widget to the order (causing the order to be updated)
      2. Remove all custom discount line items
      3. Calculate the new discount, and add a discount to the order (causing the order to be updated)
      4. Go back to step 2...

      Essentially what I wanted to do was force the Remove custom discounts rule action (which was a custom action) to only be executed once. I did not choose the most elegant way to solve this problem, and re-analyzing would have likely worked with the logic of a Rules component 'Rule set'.

      At the top of my Rules action handler I added a test, to make sure that the function would only execute once per bootstrap.

      function my_rules_action_handler($param1, $param2) {
        // Make sure this function only executes once.
        if ($executed = &drupal_static(__FUNCTION__) {
          return FALSE;
        }
        else {
          $executed = TRUE;
        }

        // Insert the code for the action.
      }

      I'm not too happy with the solution, and hope to have the opportunity to revisit this in the future. Not every solution that we come up with as developers makes us happy, but the hope is that they all successfully satisfy the problem that we are attempting to solve.

Tags:

Featurizing Drupal Commerce configuration - Commerce features

I was moving some Drupal Commerce work from a local development environment to an integration and staging server this week, and realized that Commerce really does lack in its ability to have configuration featurized. I found the Commerce features module, which initially seemed like it would be quite helpful. Unfortunately, most of the commerce configuration that I was looking at is not featurizable using this module.

What I needed to featurize were shipping services created through the UI exposed from the Commerce flat rate module, payment method configuration (rules) for some custom payment methods I created, and the placement and order of checkout panes. None of these three configurations currently appear to be featurizable, however, there are open issues for two of them.

I'm hoping to be able to find some time to contribute to the ability to featurize Commerce configuration, either in my spare time, or through client sponsored support. It appears, through reading Features Integration Checklist that there is currently some discussion around making configuration cTools exportable, so that there is no reliance on Features directly.

Getting all articles, ordered by title, with EntityFieldQuery

EntityFieldQuery is a class that can be used to... query entities... by field! And, other things too.

Today I needed to get a list of all nodes, of a certain type, ordered by title. One might go to the database for this, but I decided to do things the 'right' way, by leveraging the Drupal 7 entity system.

  // Create a new EntityFieldQuery.
  $query = new EntityFieldQuery();
  // Filter by the 'node' entity.
  $query->entityCondition('entity_type', 'node')
    // Filter by the 'article' bundle.
    ->entityCondition('bundle', 'article')
    // OrderBy the 'title' property. Yup, because 'title' is a property of Node, not a field.
    -> propertyOrderBy('title', 'ASC');
  // Execute the query and get the results.
  $result = $query->execute();
  // The result is returned as an associative array of entity types, with inner arrays keyed by entity id.
  $nids = array_keys($result['node']);
  // Now that we have all of the nids for all articles ordered by title we can load the nodes.
  $articles = node_load_multiple($nids);

I've used EntityFieldQuery before, but I must admit that I had to do some research today to figure out how to order by title. I knew that it was possible to fieldOrderBy() and discovered that it was possible to entityOrderBy() (although admittedly I don't understand entityOrderBy yet). Remembering that title is a property of Node helped me find my way to propertyOrderBy().

Using Panels and Page Manager with your eyes closed

As the Drupal Core accessibility maintainer, I from time to time have people ask me about the accessibility of different contributed modules. Several times in the past I've been asked about the accessibility of Panels (where I assume the person means cTools Page Manager as well). Only this week have I had the need to use Panels and Page Manager. This is by no means a thorough review, but my observations as a screen-reader user.

I was definitely able to get around Page Manager, to clone and modify some node_view variants. The experience wasn't something that made me super happy, indeed I yelled (quite literally) at my computer several times.

The first major problem I observed was that the 'gear' icons (used to modify regions, to add content, and to modify panels) were read as 'link #'. I didn't go looking into the DOM, but this is generally symptom of an anchor without any text. Screen-reader users should know that the 'link #' prior to a region or panel is the gear that will expose the options for the proceeding object.

The second problem was that I wasn't able to rearrange panels by dragging and dropping. This is pretty obvious, as screen-reader users don't tend to use a mouse. In Drupal Core we mitigated the accessibility barrier caused by tabledrag UIs by implementing 'row weights'. The same thing wouldn't necessarily work here, but it might be possible to expose a weight for each panel in a region through the UI. Perhaps this already exists and I'm just not finding it.

I've filed the following bugs, and will work with the cTools module maintainer to improve the accessibility of the module.

Pages