Wednesday, April 21, 2010

Showing events from multiple public Google Calendars using your own custom CSS

Today I hacked some code to produce a result I know others are looking for, so I thought I would share. So far what I have is the result of of an hour or so exploring the code to see if this was possible. In other words, there is room to make this much more elegant and I hope to do so at some point.

The seed of the issue is the inane inability to cusomize the color of the frame when embeding a Google Calendar. That really, really ought to be fixed by Google.

One solution to this problem which I found (yes, by Googling it...) is "calvis". The project home page is here: http://code.google.com/p/calvis/

Calvis uses the Google Data API to pull down calendar events and display them in on screen using JavaScript.

Unlike the embeded calendar, Calvis does not support displaying multiple Google Calendars. There is some discussion of this issue on the site's issue tracker (issue #3). This is the problem for which I now have a working solution.

Note that this solution has only been tested for the cases where all calendars used are public and will probably not work in its current form for private calendars.

Here's what I did:

The majority of the changes are within calvis-core.js and you can download the diff here.

1) I don't think it matters, but for what it's worth I replaced the projects jQuery 1.3.2 with the latest 1.4.2

2) I added a new variable, calList to the variables defined in the main() javascript funtion which is defined in-line on the calendar page. This variable holds an array of the calendars to be combined. Here's what my declaration looks like:

var calList = [ 'google@communitygreenguide.org',
'b0prga519c0g0t3crcnc0g9in0@group.calendar.google.com' ];

3) Also within the main() javascript function on the calendar page, I set the calList variable within the Calendar object, using a new setCalList() method defined in calvis-core.js (more on this in a minute).

calendar.setCalList(calList);

4) In the Calendar constructor, create a calList variable set to null:

this.calList = null;

5) Defind the setCalList() method:

calvis.Calendar.prototype.setCalList = function(calList) {
this.calList = calList;
}

6) Modify the existing getFeedUrl() method so that it returns an array of calendars and not just one calendar feed:

calvis.Calendar.prototype.getFeedUrl = function() {

feedUrlArray = new Array();
calList = this.calList;

for( var i=0; i < calList.length; i++ ) {
feedUrlArray[i] = ['http://www.google.com/calendar/feeds/',
calList[i], '/', this.visibility, '/full'].join('');
}

return feedUrlArray;
};


7) Change some code in the initLoginControl() method so that it can deal with getting an array of calendars. I don't believe this code will actually serve any purpose since we're only using public calendars, so I just use the first calendar in the array.

var feeds = calendar.getFeedUrl();
var scope = feeds[0];

8) In the two places overlayGData() is called, modify the calling code so that it calls overlayGData() once for each calendar, passing in the feedUrl as an attribute. This code appears near the bottom of the updateWeekView() method and at the bottom of the updateMonthView() method:

var feedUriArray = this.getFeedUrl();
calendar = this;
for( var i=0; i< feedUriArray.length; i++ ) {
calendar.overlayGData(firstDate, lastDate, feedUriArray[i]);
}

9) Change the method signature of overlayGData so that it will acced the feedUri as a parameter:

calvis.Calendar.prototype.overlayGData = function(startDate, endDate, feedUri)

10) Remove the line in the existing overlayGData() method that retrieved the feedUri from the object:

var feedUri = calendar.getFeedUrl();

Tuesday, November 24, 2009

Uploading Files to a webserver from CKEditor under mod_perl

Just got a request to elaborate on this topic which I had mentioned in a recent tweet. So here's some more details. Note that there is no file-type checking here. In my case the uploads are only being allowed from logged-in administrators, so I can assume they are not uploading malicious files.

1) Of course, remember to use enctype="multipart/form-data" in the HTML form. Obvious but I've forgotten it more than once over the years...

2) To tell CKEditor what URL to send the uploaded file to, I pass the target URL to the CKEditor command which replaces the form <textarea> with a rich text editor:

CKEDITOR.replace( 'form_textarea', { filebrowserUploadUrl : '/upload_file' } );

In my case I did this inside a jQuery "ready" handler:


$(document).ready(
function() {
CKEDITOR.replace( 'form_textarea', { filebrowserUploadUrl : '/upload_file' } );
}
);


3) Map the URL to a mod_perl handler

I'm using object oriented perl handlers, so the relevant portion of my httpd.conf file looks something like:


<Location ~ "^/upload_file$">
SetHandler perl-script
PerlHandler NacreData::Controllers::FormHandler->upload_file
</Location>


This will look different if you're using a procedural handler or mod_request or some such.

4) Require/Use a perl module to do the uploading

The one I got to work is Apache::Upload::Slurp

I installed this with CPAN and used a PerlModule directive in httpd.conf to make sure it was loaded at runtime.

5) Write the mod_perl handler

Here's a cleaned up version of what mine looks like:


sub upload_file( $$ ) {
my ( $self, $r ) = @_;

use Apache::Constants qw( :common );
my $apr = Apache::Request->instance( $r );
$r->content_type('text/html');
$r->send_http_header;

require Apache::Upload::Slurp;
my $Uploader = new Apache::Upload::Slurp;
my $uploads = $Uploader->uploads();
my $upload = $uploads->[0];
my $dir = '/upload_files'; # directory where the uploaded files will go
return unless $upload;
return unless $upload->{data};

# make this into a nice filename, no special chars, no spaces, etc.
my $filename = $upload->{filename};
$filename =~ m{([^\\/]+$)};
$filename = $1;
$filename =~ s/[^.\w]//g;
$filename =~ s/__+/_/g;
$filename =~ s/^_//;
$filename =~ s/_$//;
$filename ||= 'file';

# make sure we don't overwrite another file by the same name
while( -f $dir.'/'.$filename || -d $dir.'/'.$filename ) {
my $ext = '';
if( $filename =~ m/(\.[^.]+$)/ ) {
$ext = $1;
$filename =~ s/\.[^.]+$//;
}
my $d = 0;
if( $filename =~ m/(\d+)$/ ) {
$d = $1;
$filename =~ s/\d+$//;
}
$d++;
$filename = $filename . $d . $ext;
}

my $fh;
$filename = $dir.'/'.$filename;
open ( $fh, '>', $filename ) || die "can't open $filename";
print {$fh} $upload->{data};
close $fh;

# now we return the information about the uploaded file to CKEditor.

# reference passed in from CKEditor. We use this to call back, letting
# the editor know we've don the upload.
my $funcNum = $apr->param('CKEditorFuncNum');

$filename =~ s|^/htdocs||; # path under document root to uploaded file
# you probably need ot change this line -- in my case, since I'm
# running under chroot, the webroot is just "/" and docroot /htdocs

my $hostname = $r->server->server_hostname;
$hostname =~ m{(\w+\.\w{2,4})$};
my $url = 'http://' . $hostname . $filename;

print "<script>parent.CKEDITOR.tools.callFunction( $funcNum, '$url' );</script>";
}

Tuesday, September 2, 2008

BBEdit 9.0 -- First Impressions

After a little more than one working day using the newly released BBEdit 9.0, my impressions is positive.

First of all, I am glad to see that a trusted tool works largely as expected as it keeps pace. This is to me much more important than flashy new interfaces or features. The search interface is cleaner, which is nice, but over all I had no problem at all getting right back to work after the upgrade.

My favorite new feature is the variable auto-completion I noticed this evening while working on an approximately 400-line perl script with many, many variables. The auto-completion gave me great assurance without any split screens or searching that I had the spelling and capitalization of previously used variables correct.

Thanks, BareBones for the continued good work.

Friday, August 1, 2008

Product Review: Tone 12 GB USB 2.0 Mini Hard Drive

Very cute, casing matches my MacBook Pro. Works reliably so far. Took formatting to Mac (Extended, case-insensitive, journaled) no problem. Slow as molasses, though. The speed really cuts into it's utility for my intended use in loading virtual machines.

Launch VMWare and open a suspended WinXP virtual machine:

* Over the network from G5 dual 1.8GHz PowerPC to MacBook Pro: 58 seconds
(54 seconds to suspend, saving state back to disk)

* Same virtual machine from Tone 12 GB attached to MacBook Pro directly: 1:02 minute. Saving state: 1:41 - 3:09 minutes.

* Same virtual machine loading directly from MacBook Pro HD: 17 seconds; suspend: 4.4 seconds

Saturday, July 5, 2008

Green Business Leadership

We'd heard that some rental car companies are renting hybrids. Calling around today, my wife was frustrated to learn that the options are few and far between in the Triangle area. Seems that just like the car manufactures, most rental companies are followers, looking behind, instead of business leaders looking forward.

Which gets me thinking—how can NacreData LLC do a better job of being a green business leader? Feel free to leave your suggestions in the comments.

Our transportation costs are largely non-existent — I work in a home office and everyone else currently doing NacreData work is also a home-based.

We're not a bulk hosting company, but I've read a few times (see http://blogs.business2.com/greenwombat/2007/02/photo_originall.html for instance) that server farms are power hogs. We're beginning to move more of the sites we manage onto virtual-machine based servers, where several server environments can share the resources of one physical machine.

Friday, December 28, 2007

Getting SpamAssassin's spamc to work with Qmail

One of the recommended ways of setting up SpamAssassin to work with Qmail is to move the binary "qmail-queue" to a different name, say qmail-queue.orig, then create a shell script named qmail-queue which passes the email through SpamAssassin then on to the original qmail-queue. 

One problem with that set up which I've been noticing is that it passes every email through the spam filter, even though the vast majority of email coming into the server is addressed to non-existent users. The time taken to scan all those emails was noticeably slowing down the system. 

So what I really wanted to do was call the spam filter from the .qmail files for real users. 

Qmail's default delivery mechanism, set to contain just "#" in .qmail-default, will then drop all emails sent to non-existent users without calling SpamAssassin. 

I wanted to use the spamc utility, written in C, as a filter to call the already running spamd deamon from the user's spamc with the "-c" switch so that non-spam messages will return 0 (zero)  and thus pass on to delivery. One small problem, though, is that the exit value for an email determined to be spam is 1. Qmail will interpret an exit value of 1 as a temporary failure and keep trying to deliver the message. This will create a backlog of undelivered messages and slow down the email server. 

To get around this, I changed the exit value for spam emails in spamc. First I downloaded the latest SpamAssassin and untarred the download file. In the "spamc" subfolder I found a file named "libspamc.h". On or around line 85 is the line:

#define EX_ISSPAM   1

I simply changed this to 

#define EX_ISSPAM   99

and then completed the installation steps:

perl Makefile.PL
make
sudo make install

which installed SpamAssasissin including spamc with the altered exit value.

Lastly, from my user .qmail files, I call spamc. For example for .qmail-devin:

|/usr/local/bin/spamc -c
./devin/Maildir/

and now SpamAssassin is run only for email coming to real users and all messages determined to be spam are silently dropped. The return value of 99 tells Qmail that the message has been completely processed and no further delivery steps need be taken.