Tim's blog

Basic Theming: Move AddThis to the Top

Prerequisites: Drupal 6, PHP, Basic theming

By default, the AddThis contrib puts a pretty button at the bottom of nodes, enabling users to share your content with various social networks. But what if you want to show the button at the top of the node instead of the bottom? Here's a quick way to do that.

You can, of course, just skip the AddThis module and add your own code from AddThis wherever you want, but that's not the point of this post. We're going to make a few small adjustments at the theme layer in order to make it happen.

Step 1. Add a new template variable and modify the old one

This code goes in your theme's template.php file. If you don't already have a preprocess_node hook, create a function called mytheme_preprocess_node(&$variables) (substituting your own theme name, of course). The preprocess_node hook simply prepares variables for use in your node template. There are two elements of interest to us in the $variables parameter: the links string and the node object. The links string is merely the literal HTML that gets displayed (usually) at the bottom of nodes. It contains things like links to add a comment or read more, read counts, and in this case, the AddThis button. Within the node object is another links element, but this contains the items separated into an array instead of rendered together in a single string.

function mytheme_preprocess_node(&$variables) {
  if (!empty($variables['node']->links['addthis'])) {
    $variables['addthis'] = $variables['node']->links['addthis']['title'];
    unset($variables['node']->links['addthis']);
    $variables['links'] = theme_links($variables['node']->links, array('class' => 'links inline'));
  }
}

All we're doing is taking the AddThis HTML and putting it in it's own template variable, called addthis ($variables['addthis']), and then recreating the links template variable ($variables['links']) without it.

Step 2. Use the new addthis variable

This part goes in your theme's node template (e.g., node.tpl.php). Remember that addthis variable we just created? Just print it out where you want it.

<?php if ($addthis): ?>
  <div class="addthis">
  <?php print $addthis ?>
  </div>
<?php endif; ?>

For convenience, we created an addthis div and put it above the content but below the title.

Step 3. Style it with CSS

Lastly, style it with your CSS of choice. We wanted it hanging out on the right, on the same line as the Submitted by info.

.addthis {
  float: right;
}

One last thing: in order for this to take effect, you may have to rebuild your theme registry or flush your caches depending on the state of the site. You can do this with the devel or admin_menu contribs or, lacking those, go to admin/settings/performance and click Clear Cached Data.

That does it. A basic solution to a basic problem.

Don't Split My Terms

Prerequisites: PHP, Theming, Drupal 6, Regular Expressions

Adventures With Nested Regular Expression Functions

Sometimes a situation calls for skills you know you should have but just don't use often enough to claim as a weapon in your arsenal of tools. For me, that skill is competence with regular expressions. It seems like every six months or so I'm back to reading Mastering Regular Expressions and scouring the interwebs for help deciphering or creating one of those cryptic phrases.

Many thanks to regular-expressions.info, a great resource for learning about regular expressions.

Here's the scenario: a client's site has nodes that are tagged with multiple taxonomy terms, sometimes enough to make the list of terms span multiple lines. Sometimes the terms are phrases, so they contain multiple words and as a result are sometimes split across lines. We want to ensure that taxonomy terms are always kept together.

The simplest answer is probably to use display: inline-block; in the appropriate CSS declaration, but even though that's what we ended up doing, I thought this alternate solution was an interesting exercise in the use of regular expressions.

The Code

function exampletheme_preprocess_node (&$variables) {
  // Replace all spaces within <a> tags with non-breaking spaces so they don't span lines
  $pattern = '/(<a\b[^>]*>)(.*?)(<\/a>)/';
  $variables['terms'] = preg_replace_callback($pattern, create_function('$matches', 'return $matches[1] . preg_replace("/ /", "&nbsp;", $matches[2]) . $matches[3];'), $variables['terms']);
}

The code will go in your theme's template.php file, in the mytheme_preprocess_node function.

Now let's have a look at what's going on. The parameter &$variables is a keyed array, in which each of the keys eventually becomes a variable that is made available to the node template (e.g., node.tpl.php). The variable we're interested in is 'terms', which is the fully rendered list of taxonomy terms for the node. For example,

<ul class="links inline">
<li class="taxonomy_term_79 first"><a href="/category/business-categories/accessories-jewelry" rel="tag" title="">Accessories / Jewelry</a></li>
<li class="taxonomy_term_92"><a href="//category/business-categories/energy-savings" rel="tag" title="">Energy Savings</a></li>
<li class="taxonomy_term_100"><a href="/category/business-categories/home-furnishing" rel="tag" title="">Home Furnishing</a></li>
<li class="taxonomy_term_101"><a href="/category/business-categories/household-cleaning-products" rel="tag" title="">Household Cleaning Products</a></li>
<li class="taxonomy_term_106"><a href="/category/business-categories/paper-products" rel="tag" title="">Paper Products</a></li>
<li class="taxonomy_term_110 last"><a href="/category/business-categories/retail-stores" rel="tag" title="">Retail Stores</a></li>
</ul>

Our goal is to take spaces that occur within <a> tag text and replace them with non-breaking spaces (&nbsp;). For example, change Paper Products into Paper&nbsp;Products. To do that, we use a nested call to the PHP function preg_replace.

Let's look at the innermost preg_replace:

preg_replace("/ /", "&nbsp;", $matches[2])

This simply says, "find each space in $matches[2] and replace it with &nbsp;." The first parameter is a regular expression consisting of the start delimiter (a slash), a space, and an end delimiter (another slash). We'll get to $matches[2] in a sec.

The parent function is actually a slightly different form of preg_replace that takes a function as its second parameter. preg_replace_callback calls that function once for each match it finds. In our case, we define the function inline using create_function, but it can just as easily be done with a normal function declaration.

preg_replace_callback passes an array in the form of:

  • $matches[0] = the full matching text
  • $matches[1] = backreference 1
  • $matches[2] = backreference 2
  • ...
  • $matches[n] = backreference n

so, going back to the inner function, we're operating on the string passed as backreference 2. So, what's backreference 2? For that, we need to look at the regular expression:

/(<a\b[^>]*>)(.*?)(<\/a>)/

We already know that the slashes on the ends are just delimiters, so let's look at the three parenthetical clauses. Using parentheses around these clauses causes the regex engine to create backreferences which can then be referred to by our callback function.

First we have

(<a\b[^>]*>)

which matches

  • The literal "<a" as long as it is on a word boundary (\b). That's to distinguish an <a> tag from say, an <acronym> tag.
  • Followed by any number of characters as long as they are not a closed angle bracket ("[^>]*")
  • Followed by the literal ">" 

So, that's the <a> tag and all of it's attributes.

Let's skip the second one for now and look at number three:

 (<\/a>)

  • matching a literal "<"
  • followed by a "/", which needs to be escaped with a "\" first
  • followed by a literal  "a>"

That's our closing tag.

Going back to number two:

 (.*?)

which matches any character (".") any number of times ("*"). The "?" makes the star "lazy" so it will stop before the first closing tag rather than the last one. This represents the stuff in between the tags.

Lastly, we don't just need the stuff in the middle, we also need the tags, so we prepend $matches[1] and append $matches[3] to the string before we return the final value from the callback.

So there it is. A rather long explanation for a rather short amount of code, but a good opportunity to brush up on some regular expression basics.

Quick and Easy "Or Admin" Filter for Views

Prerequisites: Drupal 6, Views2, PHP

Recently, we had the need to create an Organic Groups view that filtered nodes based on group membership or, if the current user was admin, to show nodes from all groups.  We needed something like the "Published Or Admin" node filter, only applied to Organic Groups membership.

My first instinct was, "No problem! Organic Groups considers users with the administer nodes permission to be members of all groups." Unfortunately, this membership test does not apply to Views filters.

My second instinct was, "That's OK, we'll just use the user:roles filter". Unfortunately, the user:roles filter acts on the node owner rather the current user.

Contribs to the Rescue

Fortunately, there is a simple way around this using the contribs Views Or and Views PHP Filter. As you are probably aware, views filters are normally combined with logical ANDs, meaning that a node is included only if it satisfies the requirements of all the included filters. Views Or allows us to combine arbitrary filters with logical ORs instead, giving us a lot more flexibility.

Views PHP Filter allows us to execute arbitrary PHP code and return a list of node IDs as either an inclusive or an exclusive filter. For example, if you define a PHP filter that returns the nids (1, 2, 3) and then add a node:published filter, you'd get any of nodes 1, 2, and 3 that are also published. Of course, that wouldn't be particularly useful; however, since we can execute arbitrary PHP we can write some code that does this:

if (the current user is admin) then return (a list of all node ids), otherwise return nothing.

Combine that with Views Or and things start to get interesting. One fortunate thing is that if you have a null clause (like our "return nothing" case), Views Or does not generate anything in the resulting query.

Combining Views PHP Filter and Views Or

With that combination we can effectively get this kind of filter logic: 

If the current user is not admin: if (the current user is a member of the node's group) ← no OR clause since we returned nothing

If the current user is admin: if (the current user is a member of the node's group OR the nid is IN (set of all nids))

Now the problem with this approach is that it incurs an extra database call every time the page is viewed (to get a list of all nids), plus the set of all nids could potentially get really large. If you remember, though, we can configure our PHP filter to be exclusive instead of inclusive.

Using Negative Logic

Now, armed also with the knowledge that nids start at 1, we use negative logic for our PHP code:

if (the current user is admin) then return (nid 0) 

or

global $user;
if (in_array('admin', $user->roles)) {
  return array(0);
}

You see where I'm going with this. Combined with the exclusive PHP filter option, we're saying "nodes that do not have nid = 0", which is to say, all nodes in the system.

File 30

So we end up with the following filters (of course, substitute your own node type):

File 31

 That's it. With just a few lines of code and a couple of modules, you've created an "Or Admin" filter.

 

Debugging Drupal Remotely with NetBeans and LAMP

Prerequisites: PHP, NetBeans, Linux Administration

We at Tarakan Design came to the web development world after spending many years in traditional software development. One of the tools we missed was a readily available interactive debugger for our PHP scripts, particularly for remote servers. Sure, there were PHP debuggers out there, but setup was marvelously complicated and results were, well, let's just say that our mileage did vary. Fortunately, setup is now simple if you use NetBeans and a LAMP-based development server.

For these instructions, I used NetBeans v6.8 and Ubuntu Server v9.10.

Configuring the Server

The first thing you have to do is install the Xdebug extension for PHP. The good news is that installation these days is a breeze. Just open an SSH session and install it with the aptitude package manager:

$sudo apt-get install php5-xdebug

Update: you may get an error (e.g., on 10.04 LTS) saying something about php5-xdebug not being available. In that case, edit the file /etc/apt/preferences.d/lucid and change the php5-xdebug line from "karmic-updates" to just plain "karmic"

This should create the file /etc/php5/cli/conf.d/xdebug.ini. Let's have a look inside. It should already have the Zend extension defined:

zend_extension=/some/path/to/xdebug.so

We just need to add a few lines to enable it to work with NetBeans remotely:

;Enable remote debugging
xdebug.remote_enable=1
;Select the debugger protocol (php3 or dbgp)
xdebug.remote_handler=dbgp
;Select when a debug connection is initiated.
; req tells Xdebug to try to connect to the client as soon as the script starts.
; jit tells Xdebug to only try to connect on errors.
xdebug.remote_mode=req
;Set the IP address of your local machine, not the server
xdebug.remote_host=192.168.1.9
;Set the port.
xdebug.remote_port=9000

Save the file and restart Apache

$sudo /etc/init.d/apache2 restart

That's it for server configuration. Now on to the client...

Configuring the Client

All you really have to do is make sure the client and server files are in sync and tell NetBeans where they are. An easy way to do this is to start with the site files on the server and create a new NetBeans project by importing them:

  1. Start NetBeans and create a new PHP project (File->New Project). Select PHP Application from Remote Server and click Next.
  2. Give the project a name and local location and click Next.
  3. Setup the Remote configuration as appropriate for your server and click Next.
  4. NetBeans will construct a list of files it will download. Click Finish to download the files locally.

When it's done downloading, NetBeans will bring up index.php from the Drupal root. Right-click anywhere in the file and select Debug File. The debugger will automatically stop on the first line. From here, you can set any breakpoints you need by opening the source file, right-clicking on the desired line and selecting Toggle Line Breakpoint (there are a few other ways of setting breakpoints as well). Via toolbar, hotkeys or the Debug menu, you also have familiar interactive debugger functions like run, step over, step into, step out, and run to cursor. At the bottom, you have panes for watches, variables and the call stack.

And with that, we've reclaimed interactive debugging for our Drupal development toolkit. The gods of productivity will be pleased.

Multisite Aliasing in Drupal 6


Prerequisites
: multisite, patching

If you, like us at Tarakan Design, make frequent use of Drupal Multisite and your workflow involves various incarnations of development, staging and production sites, there is good news from Drupal.org.

One of the problems we face in the development of sites using this three-legged workflow lies in database migration as we move from phase to phase. Typically, the sites/ directory on a development server will contain a directory like sites/newsite-development.localserver.com. Similarly, the staging server might contain sites/newsite-staging.stagingdomain.com and finally the production site with sites/newsite.com. The problem is that when we migrate the database between any two of those installations, it contains references to the wrong site URL. Yes, it is true that well-behaved core and contributed modules only store paths relative to the site root, but what about files in the sites/newsite-staging.stagingdomain.com/files directory? Those paths are relative to the site root yet are still dependent upon the site URL.

Fortunately, there is an available patch for Drupal 6 that fixes this particular issue. This patch makes a minor alteration to bootstrap.inc that looks for a file called sites.php in the sites/ directory. The sites.php file allows you to specify site aliases that uncouple real site URLs from site URLs referenced by the multisite mechanism. For instance, in the above example, sites.php would contain: 

$sites = array(
  'newsite-development.localserver.com' => 'newsite.com',
  'newsite-staging.stagingdomain.com' => 'newsite.com',
);

Now, when you point your browser at http://newsite-development.localserver.com, Drupal will use the 'newsite.com' multisite directory even though you are on your development server. For instance, you can specify "sites/newsite.com/files" in the File System settings (admin/settings/file-system) and you will still be correctly configured across your development, staging and production installations.

Of course, you are still hosed if somebody puts a fully qualified path on your site (say, in your main site navigation menu), but we'll just call that a "personnel" issue.

Syndicate content

Our Philosophy

 

  • Customer centered, Agile development
  • Clear and open communication
  • Solid software engineering principles

 

Drupal

Drupal

 

We use the Drupal Content Management Framework for secure and reliable websites.
Learn More