Pages

2013-11-08

Getting ImageMagick to work in XAMPP on OS X Mavericks (10.9)

I recently upgraded my aging mid-2009 Macbook Pro from OS X 10.6.8 (Snow Leaopard) to 10.9 (Mavericks) and am reaping the usual problems of apps not working the same any more.  The latest debacle is that ImageMagick no longer works on my local web sites.

When I first installed ImageMagick on Snow Leopard, this post at Unreal Expectations was super helpful.  Unfortunately, things didn't go so well this time around.  This post is just a summary of the bumps I encountered along the way.

First of all, one thing to note is that upgrading XAMPP by running the installer for a newer version on top of an already-installed version is unreliable in my experience.  I made the mistake of not backing up the whole /Application/XAMPP folder, and ended up losing all my database tables.  Woops!  What I should have done is run a mysqldump on my databases to export all my data into sql files that I could then later import, something like so: mysqldump -u root -p a_database_name > the_data.sql .  Alas, I did not, and I have paid for it dearly.

Anyway, so I got to the point of deciding to just uninstall XAMPP and start from scratch.  I got the XAMPP 1.8.3 installer from the official website, and installed it.

First problem: re-activating my virtual hosts.

One thing I did do was save my httpd-vhosts.conf file, and all the symbolic links I'd used in the htdocs folder, so I moved those back into their respective locations in etc/extra and htdocs.

Having done so, I tried loading one of my local sites, and all it did was redirect me to the XAMPP control panel.

I'd forgotten to restart apache.  *facepalm*  So I restarted apache using the XAMPP OS X Manager app.

Still nothing.

I'd forgotten to uncomment this line in /Applications/XAMPP/etc/httpd.conf *facedesk*:
Include etc/extra/httpd-vhosts.conf

Problem #2: 403 Error

I was all excited to see my site upon a browser refresh, but I was greeted with a 403:

Access forbidden!
You don't have permission to access the requested object. It is either read-protected or not readable by the server.
If you think this is a server error, please contact the webmaster.
Error 403
There were two things going on here.  First, I hadn't reset my folder permissions in htdocs to make them read/executable by the server process.

To fix this:

sudo chgrp -R www /Applications/XAMPP/htdocs
sudo chmod -R 775 /Applications/XAMPP/htdocs
I also needed to add a Require all granted directive to my httpd-vhosts.conf file.  One note here: in my previous installation I didn't need this directive because I had included it in my httpd.conf file like so:

<Directory />
    AllowOverride none
    Require all granted
</Directory>

But that opens up access to the root server directory to anyone, which doesn't seem good.  It's not so bad because I never have this server listening on a public port, but still, I should learn good practices, and the right way to do this is to use the directive in the vhost config itself, like so:

(In httpd-vhosts.conf)
<VirtualHost *:80>
    ... other vhost stuff ...
    <Directory "/Applications/XAMPP/htdocs/xampp">
        Options FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

So after restarting apache again, the site loads!  Huzzah!

Progress: Re-installing ImageMagick

There are two parts to installing ImageMagick for use in apache.  First, you install the ImageMagick application.  I used MacPorts to do so:

sudo port install ImageMagick

Then you need to install the imagick extension.  I used pecl:

sudo pecl install imagick

Pecl should stick a file called imagick.so into /Applications/XAMPP/xampfiles/lib/php/extensions/no-debug-non-zts-<somenumber>/.  You may need to change the file permissions of this file if it's not executable already:

sudo chmod 755 /Applications/XAMPP/xampfiles/lib/php/extensions/no-debug-non-zts-20100525/imagick.so

And make sure there is a line like this in /Applications/XAMPP/etc/php.ini:

extension="imagick.so"

And while you're in php.ini, make sure that extension_dir is set to the path to the folder in which imagick.so is now sitting.

Problem the next: imagick.so doesn't load

After restarting apache and attempting to load a page that uses ImageMagick, I got the classic error telling me that ImageMagick still isn't working:

Fatal error: Class 'Imagick' not found in blah blah blah

I checked my phpinfo (by using the php -i command) and noticed that indeed there is no reference to imagick anywhere.  Digging a bit deeper, I checked my php error log (the location of which is determined by the error_log setting in the php info) and noticed the following error:

PHP Warning:  PHP Startup: Unable to load dynamic library '/Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20100525/imagick.so' - dlopen(/Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20100525/imagick.so, 9): Library not loaded: /opt/local/lib/libfreetype.6.dylib
  Referenced from: /opt/local/lib/libMagickWand-6.Q16.1.dylib
  Reason: Incompatible library version: libMagickWand-6.Q16.1.dylib requires version 17.0.0 or later, but libfreetype.6.dylib provides version 15.0.0 in Unknown on line 0

The problem here is that the ImageMagick library I'm using links a different version of libfreetype.6.dylib than what is used in my XAMPP installation.  The fix is to copy the Macports version of this library into the XAMPP directory (but make a backup of the XAMPP one before you do this, of course):

cp /opt/local/lib/libfreetype.6.dylib /Applications/XAMPP/xampfiles/lib

Problems, more problems: apache no longer restarts

When I went to try restarting apache this time, it didn't even stop for me any more.  So I went to the commandline:

apachectl start

Yielded a similar error as before.  This time it's complaining about libpng:


Syntax error on line 522 of /Applications/XAMPP/xamppfiles/etc/httpd.conf: Syntax error on line 10 of /Applications/XAMPP/xamppfiles/etc/extra/httpd-xampp.conf: Cannot load modules/libphp5.so into server: dlopen(/Applications/XAMPP/xamppfiles/modules/libphp5.so, 10): Library not loaded: /opt/local/lib/libpng15.15.dylib\n  Referenced from: /Applications/XAMPP/xamppfiles/lib/libfreetype.6.dylib\n  Reason: Incompatible library version: libfreetype.6.dylib requires version 33.0.0 or later, but libpng15.15.dylib provides version 25.0.0

So I do the same thing as previously, backing up the libpng in the XAMPP folder and moving in the Macports one:

cp /opt/local/lib/libpng15.15.dylib /Applications/XAMPP/xampfiles/lib

Now apache starts and stops happily.  But there's another php error now again.  This time we need to update libexpat:

cp /opt/local/lib/libexpat.1.dylib /Applications/XAMPP/xampfiles/lib

And at last, I have a functioning apache and a functioning ImageMagick extension in OS X Mavericks!  That seemed like too much work.

2013-09-20

Disappearing Advanced Custom Fields

So at work today I received an email from a client who had just run the WordPress upgrade from 3.6.0 to 3.6.1 for a site we support.  Some fields managed by the Advanced Custom Fields plugin had disappeared after the upgrade.  Great...

I dug around for a bit in the admin site and discovered that most of the ACF fields remained intact, but 3 Text fields and one Yes/No field was no longer showing up from one particular Field Group we had set up.

So I took a look at the database, and here is what I learned:

ACF Tables

ACF is written in a way that allows it to work without creating additional tables in your WordPress instance.  This presumably gives it the advantage of sneaking easily through WordPress upgrades (although the topic at hand makes me wonder about this...), but it makes for some wonky use of the standard WordPress tables.

ACF stores all its data in the wp_postmeta table, except for Field Groups, which are in the wp_posts table with post_type='acf'.  Fields are associated with their Field Groups via the post_id of the main Field entry in wp_postmeta

ACF Records in wp_postmeta

Each ACF Field has one main record in the wp_postmeta table, plus two records for each use of that Field in a post or page or whereever you set the ACF Field to be used.  Let's take a look at the records associated with one of the ACF Fields that still existed in the WordPress instance I was dealing with, the Description field:

In wp_postmeta there is one record with the following data:

meta_id: 41
post_id: 15
meta_key: field_51e859f459a64
meta_value: a:12:{s:3:"key";s:19:"field_51e859f459a64";s:5:"label";s:11:"Description";s:4:"name";s:11:"description";s:4:"type";s:8:"textarea";s:12:"instructions";s:37:"Enter the description of this product";s:8:"required";s:1:"1";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:9:"maxlength";s:0:"";s:10:"formatting";s:2:"br";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:19:"field_51e86279a9bee";s:8:"operator";s:2:"==";s:5:"value";s:1:"1";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:1;}

The meta_id is just the unique identifier for this record in the wp_postmeta table.
The post_id is the id of the Field Group post in the wp_posts table to which this Field is associated.
The meta_key is a unique identifier generated by ACF for this field.
Finally, that scary looking meta_value is just a serialized PHP array that constitutes the settings for the ACF Field.  (If you look closely, you can see that this particular field has the label 'Description' and is a 'textarea' field.  This field happens to be conditional on another field, which manifests down in the "rules" sub-array.  You can see the field id which this field is dependent on, and that it's dependent on the other field having a value of 1, and so on.  The point of this post isn't to go too far into the bowels of ACF meta data, though.  Just enough to fix the problem that I had.  So let's move on.)

There are also a bunch of records that look like this in the database (I'll leave out meta_id here since we already know what that means):

post_id: 14
meta_key: description
meta_value: This is a description

and this:

post_id: 14
meta_key: _description
meta_value: field_51e859f459a6

There is one pair like this for each use of the field on the web site.  Here, the post_id is not the id of the Field Group any more, but rather the id of the actual post in which this ACF Field is being used (this could be a page id or taxonomy_term id too, depending on the ACF Field's settings).  The meta_key in the first case is just the 'name' you see in the meta_value of the main ACF record above (a la s:4:"name";s:11:"description";).  The meta_value in the first case is the value of this instance of the field.

Now why that second record?  Well, there needs to be a way to associate this instance of a description field with the main description field record, and the second record here serves that purpose.  I'm guessing that the '_' is just a convention used by the ACF author to make it easy to determine which record is contains the actual value and which one contains the association info.

Armed with that info, we're ready to tackle the problem I encountered.

The Problem

When I looked at the wp_postmeta records associated with one of my missing fields (let's use the 'brochure_url' field as an example), I noticed that while there were indeed still lots of 'brochure_url' and '_brochure_url' records, there was no longer a 'field_xxxxxxxxxxxxx' record for this field.  Uh oh!

I have no idea why that record went missing.  Perhaps the upgrade script tried to do some database cleaning that doesn't mix well with ACF, or perhaps my client (or even myself?) accidentally deleted the field in the admin interface.  Either way, for each of my missing fields, the main ACF Field record was gone but all the data pair records were still there.

The Fix

The fix turned out to be fairly easy, with one little trick requiring some work directly in the database.  All I did was go back to the WordPress admin and recreate the missing fields, being careful to use exactly the same name as before.  (I could easily determine the name since the data pair records were still in the database.)

After recreating the fields, they all have unique identifiers that no longer matched the ones that were used by these fields before.  For example, my '_brochure_url' records all had the meta_value 'field_51e859f459a64', but my new main record for brochure_url has the meta_key 'field_51e860fa1ed53'.

Now for the trick:  I went into my database (was stuck with phpMyAdmin), found the main field_51e860fa1ed53 record in wp_postmeta, and changed two bits of data to 'field_51e859f459a64':  the meta_key, and the corresponding portion in the serialized meta_value for this record.

Et voila!  The site was back to normal.

So just to be clear, here's the 'before' picture:

The association record:

meta_key: _description 
meta_value: field_51e859f459a64

The main field record:

meta_id: 41
post_id: 15
meta_key: field_51e860fa1ed53
meta_value: a:14:{s:3:"key";s:19:"field_51e860fa1ed53";s:5:"label";s:12:"Brochure URL";s:4:"name";s:12:"brochure_url";s:4:"type";s:4:"text";s:12:"instructions";s:35:"A URL to a brochure pdf or web site";s:8:"required";s:1:"0";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:7:"prepend";s:0:"";s:6:"append";s:0:"";s:10:"formatting";s:4:"none";s:9:"maxlength";s:0:"";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:4:"null";s:8:"operator";s:2:"==";s:5:"value";s:0:"";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:5;}

And here's after:

The association record:

post_id: 14
meta_key: _description
meta_value: field_51e859f459a64

The main field record:

meta_id: 41
post_id: 15
meta_key: field_51e859f459a64
meta_value: a:14:{s:3:"key";s:19:"field_51e859f459a64";s:5:"label";s:12:"Brochure URL";s:4:"name";s:12:"brochure_url";s:4:"type";s:4:"text";s:12:"instructions";s:35:"A URL to a brochure pdf or web site";s:8:"required";s:1:"0";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:7:"prepend";s:0:"";s:6:"append";s:0:"";s:10:"formatting";s:4:"none";s:9:"maxlength";s:0:"";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:4:"null";s:8:"operator";s:2:"==";s:5:"value";s:0:"";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:5;}

Note the two places that I had to change in the main ACF Field record so that the field's unique id corresponded to the old unique id.

2012-02-12

Installing AmberTools 1.5 on OS X 10.6 (Snow Leopard)

I'm doing some volunteer work in the Meiering lab at UWaterloo this winter, and one of the packages I need to do molecular simulations and analysis is AmberTools, a set of command line tools that complement Amber.  For my purposes, I just need AmberTools in order to manipulate Amber files I get from the lab.  (I don't need Amber itself.)

I'm installing AmberTools 1.5 on my MacBook Pro with OS X Snow Leopard (10.6.8).  Here are some notes about the process.

Warning:  This should not be read as a guide to installation because I'm documenting my mistakes as I go.  Best to read the whole thing first and learn from my mistakes if you're working at installing AmberTools yourself.

Environment Variables
I need to set the AMBERHOME variable and include $AMBERHOME/bin in my path. I added the following lines to my .profile:
export AMBERHOME=[path to my AmberTools dir]
PATH="${AMBERHOME}/bin:${PATH}"

X11
X11 needs to be installed in order to build AmberTools.  In addition, the AmberTool configure script isn't looking in the right place to detect X11 on my machine.  I had to add a line (line 4 below) to the X11 detection code in the configure script:
if [ -r "$xhome/lib/libXt.a"  -o -r "$xhome/lib/libXt.dll.a" \
     -o -r /usr/lib/libXt.so \
     -o -r /usr/lib64/libXt.so \
     -o -r /usr/X11/lib/libXt.dylib \
     -o "$x86_64" = 'yes' -a -r "$xhome/lib64/libXt.a" ]

What?  No gcc?
I guess when I installed Xcode I forgot to include unix developer tools in the installation setup because my machine wasn't able to find gcc.  Blast!  Ok... Download Xcode 4; install, and make sure the Unix Developer Tools option is included in the installation setup.  Good.

Also need gfortran...
So gcc is up and running.  Great.  Back to my AmberTools directory and 
./configure -macAccelerate gnu
which eventually yielded the message
gfortran: command not found
Looks like I need to install a Fortran compiler. Done.

...and libgfortran 
Try the configure command again...a new message:
ld: library not found for -lgfortran
Ah. My Fortran libraries are not being seen. Probably some sort of environment variable / path assumptions not being met. Creating a soft link to the newly installed libgfortran in my /usr/local/lib directory did the trick for me:
sudo ln -s /usr/local/gfortran/lib/gcc/x86_64-apple-darwin10/4.6.2/libgfortran.dylib /usr/local/lib/libgfortran.dylib
Now the configuration step passes!  Woo!

Patch AmberTools 
So then I did a
sudo make install
and the build failed with a bunch of messages like this:
Warning: Type mismatch in argument 'isp' at (1); passed REAL(8) to 
INTEGER(4) 
_amg1r5.f:3619.17: 

   external cgalf,cgeps,ctime 
                 1 
Error: Return type mismatch of function 'cgalf' at (1) (REAL(8)/REAL(4)) 
_amg1r5.f:3619.23: 

   external cgalf,cgeps,ctime 
                       1 
Error: Return type mismatch of function 'cgeps' at (1) (REAL(8)/REAL(4)) 
make[1]: *** [amg1r5.o] Error 1 
make[1]: Leaving directory `/home/own/Documents/amber11/AmberTools/src/pbsa' 
make: *** [serial] Error 2 
Well okay then. After some googling and reading of mailing list threads I found a page of bugfixes for AmberTools from which I downloaded bugfix.all to my AMBERHOME directory and applied with the command
patch -p0 -N <bugfix.all
(There was also some indication that AmberTools does not compile under gfortran 4.6, which is what I just installed, but I decided to just try building after the patches were applied before worrying about building an older fortran compiler.  Happily, the patches seemed to work resolve the above error.)

The build!
And here we go again...
./configure -macAccelerate gnu
...and this time it passes and I notice this message:
The next step is to type 'make serial'
So let's do that:
sudo make serial
Fail:
AMBERHOME is not set.  Assuming it is /Users/rod/Documents/Research/Meiering/amber11
 Using AmberTools' python
Error importing MMPBSA python modules! MMPBSA.py will not work.
But I thought I set AMBERHOME above... grumble grumble google grumble google, aha! The environment isn't retained by default on a sudo; I need to use the -E option, like so:
sudo -E make serial
Aaaaand yes! It builds successfully now.

2012-02-04

A PubNub Stub

Stumbled across PubNub at work the other day.  Neat stuff.  Really easy to create distributed web apps.  Observe:

<html>
<body>
<div id="out">Click subscribe to subscribe to the messages sent by the "Send message" button.</div>
<form>
<button onclick="subscribe(); return false;">Subscribe</button>
<button onclick="sendMessage(); return false;">Send message</button>
</form>

<div pub-key="demo" sub-key="demo" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
<script src="http://cdn.pubnub.com/pubnub-3.1.min.js"></script>
<script src="demo.js"></script>
</body>
</html>
And the javascript:
function subscribe() {
    PUBNUB.subscribe({
        channel     :   "test",
        error       :   function() { alert("Connection lost.  Will auto-reconnect when Online."); },
        callback    :   function(msg) { PUBNUB.$("out").innerHTML = msg; },
        connect     :   function() { alert("Connected!"); }
    });
}
var i = 0;
function sendMessage() {
    PUBNUB.publish({
        channel     :   "test",
        message     :   "You've clicked the button "+i+" time"(i==1?"":"s")+"!"
    }); 
    i = i + 1;
}
Let's break it down a bit:

The HTML's pretty straightforward.  A div that we'll alter with the innerHTML property, and a couple buttons tied to the subscribe and sendMessage functions, respectively.  (We could just as easily subscribe on page load.)

The javascript is the interesting bit.  There are really only two PubNub API calls you need to know:  subscribe and publish.  Both functions take an object requiring a few simple properties.

Subscribe:

  • channel - Any name you like.  You subscribe to a channel, and any messages published to that channel are received as the argument to the function attached to the callback property.
  • error - A callback for when errors occur.
  • callback - The function that gets called when a message is published to that subscribed-to channel
  • connect - A callback that notifies you when the connection to PubNub has been established
Publish:
  • channel - The name of the channel to publish to
  • message - The message to send
Simple as that!  You'll note that they've even provided jQuery-like access to the DOM, so you can do things like
PUBNUB.$("out").innerHTML = msg.

You can get publisher and subscriber keys that you put in the pub-key and sub-key attributes of the pubnub div by going to pubnub.com.  Go ahead and try loading the code above in a couple browsers.  In a couple browsers on different machines, even, and enjoy the simplicity of distributed communication with PubNub!


2012-01-01

Resolution Reminders with Google Apps

In an attempt to increase my chances of success, I've decided this year to set up a periodic email reminder of my new year's resolutions.  This may backfire and only serve to remind me of my failure, but I shall choose optimism at the outset and sally forth bravely!  I've been using Google Docs lately for most of my word processing and spreadsheet needs, and I noticed recently that it is possible to add scripts to spreadsheets.  I wanted to try it out, and this email reminder job sounds like a good opportunity to do so.

So I created a new document containing my resolutions.  Now I need to email this doc to myself periodically.  Unfortunately you can't yet create scripts on docs.  You have to use a spreadsheet, so I'll have to create a new spreadsheet as well and access my script from there.

From my spreadsheet, I open the Tools → Script editor... menu, and a new window opens.  This is the Google Apps Script Editor.

Scripts (which also seem to be referred to as "projects", especially when referring to the naming of the script/project) are a bit more complex than normal Google files like docs or spreadsheets.  Each script is associated with one (and only one) spreadsheet, although one spreadsheet may have multiple scripts (accessible through the Script manager also under the Tools menu).  If I was interested in doing fancy things triggered by various events in a fancy spreadsheet, I could use multiple scripts to do so, but for the task at hand I really only need one script.

A script itself may contain any number of "files".  This allows complex scripts to be broken up in a somewhat modular way.  The script in question, though, is again simple enough that I only need the single file that Google creates by default, named "Code".  I'll also give my script a name by clicking on the "Untitled project" title field.  Now I'm ready to do some scripting!

In the main text area, the Code file is open, and Google has created a default function named myFunction, which I'll go ahead and change to remindMe.  The main task of this function will be to send me an email. Let's see if I can get a simple email sending off.

The Google Apps Script Guide contains a very handy reference of the classes and methods available to Google Apps Scripts.  The MailApp class from Mail Services looks like just the thing I'm looking for.  The simplest invocation of the MailApp.sendEmail method takes a recipient, a subject, and a message body.  I'll try a quick throw-away email to see if something works:

function remindMe() {
  MailApp.sendEmail("xxxx@xxxx.com", "testing123", "did this work?");
}


Now I'll click Run → remindMe to run the script.  But what's this?



Ah, Google needs me to authorize the script to run, and gives me some useful information about the kinds of things the script is doing (note the bottom bit about requiring the ability to send email).  Ok, so authorize away.  Now try running again...and check my email...and it works!  There's an email in my inbox with the subject and message that I was expecting:


Great!

So now to send the contents of my resolutions file.  Well, there is also the DocumentApp class, which gives me access to the Google Docs application.  The main method I'm interested in is openById.  But I need the id of my resolutions doc.  Well, if I load the doc in my browser and take a look at the address bar, I see a big string of random-looking symbols.



I bet this is the id I'm looking for.  Let's see...

function remindMe() {
  var doc = DocumentApp.openById("1qNQQbExvz_vB-0mQILu91846gySwA_ceN52cVz8GjcE");

  var subject = "Reminder: " + doc.getName();

  MailApp.sendEmail("xxxx@xxxx.com", "testing123", "did this work?");
}


Now here's a cool thing about the script editor:  there is a debugger, and I can set break points in my script by clicking with my mouse on the line number on the left side of the text area for the open script file.  When I do so, a little red dot appears, indicating that a breakpoint has been set.  If I click on the same line number again, the dot disappears, indicating that I have removed the breakpoint.  Let's see what happens if I set a breakpoint on the MailApp.sendEmail line.  This time, instead of clicking the Run button, I'll click the Debug button (the little bug icon next to the Run button).  When I do so, four new panes appear at the bottom of the script editor.



The leftmost pane shows me the file, function, and line number I'm on (note that the line number corresponds to the line on which I put the breakpoint).  The next pane lists the variables that are visible.  Oh look!  There are the doc and subject variables, and in the next two panes I can see the type and value of these variables, respectively.  As you can see, the subject string contains the title of my resolutions file, which I obtained using the getName method on the doc that I opened using the openById method and the id I found in my browser's address bar.  It looks like we've got the right file!  Beautiful.

Finally, I want to include an easy way to view my doc from my email.  I can do this in one of two ways:  include the link for the doc as part of the message body using the, or include the doc as an attachment.  I'll go with the link, as there's no need to fill my inbox with copies of my doc, but I'll show you how to do the attachment as it's a little more interesting.

The sendMail method has an optional Advanced Arguments parameter, through which key/value pairs can be passed.  The advanced arguments parameter for sendMail accepts an "attachments" key with the value being an object to attach to the email.  In this case, I can simply use the doc variable and Google will automagically package my doc as a pdf and send it along in the email as an attachment.

So here's my final code (including both the link and the attachment):

function remindMe() {
  var doc = DocumentApp.openById("1qNQQbExvz_vB-0mQILu91846gySwA_ceN52cVz8GjcE");

  var subject = "Reminder: " + doc.getName();

  MailApp.sendEmail("rod@pennyjar.ca", subject, "Have you been a good boy? " + doc.getUrl(), {attachments: doc});
}


But there's one more thing to do.  I want this email to come at me once every week to remind me of my resolutions regularly.  I can do this by adding a "trigger" to the script.  I'll click on Triggers → Current script's triggers... and then clicking on Add a new trigger.  I'll chose the remindMe function to run, and I'll make it a "Time-driven" event rather than "From spreadsheet", and set the time period to my liking.



Finally, I'll click the Save button, save my script, and save my spreadsheet, and I'm done!  Now I'll be getting a weekly reminder of my new year's resolutions.  I think I'll be wanting to lower that frequency to monthly, but it appears that weekly is as long as Google will allow me to go for the moment...

Google Apps Scripts look quite powerful.  With the additional feature that you can make your scripts public as services (Share  Publish as service... from the script editor), and the script gallery (Tools → Script gallery... from the spreadsheet editor), a lot of advanced activities in Google's suite of applications become possible and even easy.  I like it!