Entries tagged “reviewsby.us”

Resolving Django dumpdata errors

Recently I recieved this wonderful piece of news when I ran ./manage.py dumpdata for the first time:

Error: Unable to serialize database: User matching query does not exist.

I knew this might not work out since I was dealing with a legacy database, but the resolution is quite simple. First I had to narrow it down to which app was causing this. Naturally I assumed it was one of the two apps I had, either common or restaurant. So I ran: ./manage.py dumpdata common and ./manage.py dumpdata restaurant. The latter had no problem whatsoever.

This made sense, since my common application was the only one that made any reference to a User. By looking in my models.py for that application, I narrowed it down to my Profile object. Sure enough, commenting it out meant I could get my data.

It ended up being a foreign key mismatch between the profile and user tables. Since this is legacy data, this mismatch made sense. A simple SELECT id,userid FROM profile WHERE userid NOT IN (SELECT id FROM auth_user) gave me a list of bad profiles. Removing them allowed me to create my Django fixtures.

Read full post
py vs php: stemming

I’ve been porting some PHP to python during SuperHappyDevHouse and was amazed at how little code I needed to write since python makes list manipulation a breeze.

Read full post
GeoDjango

[tags]geodjango, django, gis[/tags]

Read full post
FBML and embedded CMS

[tags]fbml, css, reviewsby.us, partials, symfony, sfFacebookPlatformPlugin[/tags]

Read full post
FBML and embedded CMS

[tags]fbml, css, reviewsby.us, partials, symfony, sfFacebookPlatformPlugin[/tags]

Read full post
Facebook Markup Language: the Dashboard and Action links

[tags]facebook, fbml, apps, reviewsby.us, symfony, sfFacebookPlatformPlugin, plugins[/tags]

Read full post
Facebook Markup Language: the Dashboard and Action links

[tags]facebook, fbml, apps, reviewsby.us, symfony, sfFacebookPlatformPlugin, plugins[/tags]

Read full post
Dynamically adjusting your page title in symfony

[tags]view, view.yml, symfony, reviewsby.us, title, seo[/tags]

Read full post
Dynamically adjusting your page title in symfony

[tags]view, view.yml, symfony, reviewsby.us, title, seo[/tags]

Read full post
Caching REST with sfFunctionCache

[tags]geocoding, caching, REST, symfony, Cache_Lite, php, cache, sfFunctionCache[/tags]

For reviewsby.us we do a lot of geocoding. To facilitate we use Yahoo! Geocoding API. This helps us normalize data, obtain latitude and longitude, interpret location specific searches.

These REST queries happen a lot and will continue to happen, but this data that Yahoo! provides is fairly static. We’re basically querying a database of sorts. So it makes sense that we should cache this data.

We’ll demonstrate how to cache these queries using symfony’s sfFunctionCache class.

I wrote a wrapper (I’ll release it as a plugin if requested) for the Geocoding API, the bulk of the work (the REST call) occurs in a function called doQueryGIS:

The call to this function is always wrapped with queryGIS:

This wrapper creates a sfFunctionCache objet and calls the function and caches it for subsequent queries.

What this means is once Yahoo! teaches reviewsby.us that India is located at (25.42°, 77.830002°) and that the precision is ‘country’ we remember it in the future.

These features will be incorporated into future versions of reviewsby.us.

Read full post
Parsing a list of Key:Value pairs

[tags]best practices,php,openID[/tags] [PHP]: http://php.net/ [openID]: http://openid.net/ [reviewsby.us]: http://reviewsby.us/ [symfony]: http://www.symfony-project.com/

Read full post
OpenID

OpenID is a wonderful concept. If I visit a web site, Acme Widgets, I only need to supply a URL that belongs to me in order to log in. The web site at that URL will provide a place where I can authorize Acme Widgets to log me in with this id.

The benefit of this type of identity system is now you don’t need to create new username’s and passwords for each site you’d like to use. We initially began reviewsby.us with just OpenID for that reason. Now we can target places like livejournal and make it exceptionally easy for their users to register with our site.

As of this writing, and even more robust system is being developed, whobar. Whobar supports multiple identification sites, not just OpenID. Rather than walking through explaining how to do a sign-in system that integrates OpenID, I’ll direct people to whobar. It is in the plans for reviewsby.us to integrate whobar and once that happens you can expect a shiny tutorial and/or plugin for symfony.

Read full post
OpenID

OpenID is a wonderful concept. If I visit a web site, Acme Widgets, I only need to supply a URL that belongs to me in order to log in. The web site at that URL will provide a place where I can authorize Acme Widgets to log me in with this id.

The benefit of this type of identity system is now you don’t need to create new username’s and passwords for each site you’d like to use. We initially began reviewsby.us with just OpenID for that reason. Now we can target places like livejournal and make it exceptionally easy for their users to register with our site.

As of this writing, and even more robust system is being developed, whobar. Whobar supports multiple identification sites, not just OpenID. Rather than walking through explaining how to do a sign-in system that integrates OpenID, I’ll direct people to whobar. It is in the plans for reviewsby.us to integrate whobar and once that happens you can expect a shiny tutorial and/or plugin for symfony.

Read full post
Coming soon to reviewsby.us

In August I took a break from reviewsby.us only to be plagued by spam. In September, I relinquished portions of the project planning to my wife. We haven’t released anything publicly, yet, but there’s a lot in development.

  • I updated the development framework to symfony 1.0 alpha and took care of a whole slew of bugs.
  • Katie and I came up with a wireframe that details some of the upcoming changes.
  • I upgraded the user logic to take advantage of sfGuardUser, a user management plugin for symfony.

I’m in progress of writing location specific searches. I’m slow to implement. It seems that this month is far busier than I’d like, and I can rarely get in a block of enough time to just crank this out. The problem with geographic-specific searches is mySQL supports those types of queries, but it’s not as easy as I’d like. Zend Search Lucene with some support from PHP, however, may yield some promising results. As always, I’ll share my findings in a forthcoming tutorial.

Anyway, no visible updates on the actual site, since I didn’t want to put alpha software on the live site. I’m sure by next month symfony will be ready.

Read full post
Coming soon to reviewsby.us

In August I took a break from reviewsby.us only to be plagued by spam. In September, I relinquished portions of the project planning to my wife. We haven’t released anything publicly, yet, but there’s a lot in development.

  • I updated the development framework to symfony 1.0 alpha and took care of a whole slew of bugs.
  • Katie and I came up with a wireframe that details some of the upcoming changes.
  • I upgraded the user logic to take advantage of sfGuardUser, a user management plugin for symfony.

I’m in progress of writing location specific searches. I’m slow to implement. It seems that this month is far busier than I’d like, and I can rarely get in a block of enough time to just crank this out. The problem with geographic-specific searches is mySQL supports those types of queries, but it’s not as easy as I’d like. Zend Search Lucene with some support from PHP, however, may yield some promising results. As always, I’ll share my findings in a forthcoming tutorial.

Anyway, no visible updates on the actual site, since I didn’t want to put alpha software on the live site. I’m sure by next month symfony will be ready.

Read full post
Cropping Images using DHTML (Prototype) and symfony

Note: Like many of my tutorials, you don’t need symfony, just PHP. However, I develop in symfony and take advantage of the MVC-support that it offers.

Years ago when I was working on a photo gallery for davedash.com I got the art of making tumbnails down fairly well. It was automated and didn’t allow for specifying how the thumbnail should be made. With dozens of photos (which was a lot back then), when would I find that kind of time.

Flashback to today, for my company… we want users with avatars… but nothing too large. Maybe a nice 80x80 picture. Well the coolest UI I’ve seen was Apple’s Address Book which let you use this slider mechanism to crop a fixed sized image from a larger image.

Here’s a demo.

Overview

The front-end GUI is based on code from digg which is based on the look and feel (as near as I can tell) from Apple.

The GUI provides a clever visual way of telling the server how to chop the image. The gist is this, sliding the image around and zooming in and out change a few form values that get passed to another script which uses this data to produce the image.

Frontend: What would you like to crop?

In this tutorial, we’re going to be cropping an 80x80 avatar from an uploaded image. The front-end requires the correct mix of Javascript, CSS, HTML and images. The Javascript sets up the initial placements of the image and the controls. The CSS presents some necessary styling. The images makeup some of the controls. The HTML glues everything together.

HTML

Let’s work on our HTML first. Since I used symfony, I created a crop action for a userpics module. So in our cropSuccess.php template:

<div id="ava">
	<?php echo form_tag("userpics/crop") ?>
		<div id="ava_img">
			<div id="ava_overlay"></div>
			<div id="ava_drager"></div>
			<img src="<?php echo $image ?>" id="avatar" />
		</div>
		<div id="ava_slider"><div id="ava_handle"></div></div>
		<input type="hidden" id="ava_width" name="width" value="80" />
		<input type="hidden" id="ava_x" name="x" value="100" />
		<input type="hidden" id="ava_y" name="y" value="100" />
		<input type="hidden" id="ava_image" name="file" value="<?php echo $image ?>" />
		</div>
		<input type="submit" name="submit" id="ava_submit" value="Crop" style="width: auto; font-size: 105%; font-weight: bold; margin: 1em 0;" />
	</form>
</div>

Right now a lot of this doesn’t quite make sense. If you attempt to render it, you will just see only the image. As we add the corresponding CSS and images it will make some more sense.

CSS and corresponding images

We’ll go through each style individually and explain what purpose it serves in terms of the GUI.

#ava is our container.

#ava {
	border: 1px solid gray;
	 width: 200px;
}

#ava_img is the area that contains our image. Our window for editing this image 200x200 pixels. If we drag out image out of bounds we just want the overflowing image to be clipped. We also want our position to be relative so any child elements can be positioned absolutely with respect to #ava_img.

#ava_img {
	width: 200px;
	height: 200px;
	overflow: hidden;
	position: relative;
}
overlay

#ava_overlay is a window we use to see what exactly will be our avatar. If it’s in the small 80x80 window in the center of the image, then it’s part of the avatar. If it’s in the fuzzy region, then it’s getting cropped out. This overlay of course needs to be positioned absolutely.

#ava_overlay {
	width: 200px;
	height: 200px;
	position: absolute;
	top: 0px;
	left: 0px;
	background: url('/images/overlay.png');
	z-index: 50;
}

#ava_drager is probably the least intuitive element (Heck, I’m not even sure if I’ve even got it right). In our demo you’re not actually dragging the image, because you can drag anywhere within the #ava_img container and move the image around. You’re using dragging an invisible handle. It’s a 400x400 pixel square that can be dragged all over the container and thusly move the image as needed.

#ava_drager {
	width: 400px;
	height: 400px;
	position: absolute;
	z-index: 100;
	color: #fff;
	cursor: move;
}

#avatar is our image, and since it will be moving all around the window, it requires absolute positioning.

#avatar {
	position: absolute;
}
overlay
overlay

#ava_slider and #ava_handle are our slider components. They should be self-explanatory.

#ava_slider {
	width: 200px;
	height: 27px;
	background: #eee;
	position: relative;
	border-top: 1px solid gray;	
	background: url('/images/slider_back.png');
}
#ava_handle {
	width: 19px;
	height: 20px;
	background: blue;
	position: absolute;
	background: url('/images/handle.png');
}
Internet Explorer

PNG do not work so well in Internet Explorer, but there is a small trick, adding these components into a style sheet that only IE can read will make things work:

#ava_overlay {
  background: none;
  filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/ui/cropper/overlay.png', sizingMethod='crop');
}

#ava_handle {
  background: none;
  filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/ui/cropper/handle.png', sizingMethod='crop');
}

The Javascript

The Javascript is actually not as complicated as you’d expect thanks to the wonder of prototype. This framework provides so much so easily. You’ll need to include prototype.js and dom-drag.js.

So let’s take a look.

<script type="text/javascript" language="javascript" charset="utf-8">
// <![CDATA[
function setupAva() {
	if ($("avatar")) {
		var handle = $("ava_handle");
		var avatar = $("avatar");
		var drager = $("ava_drager");
		var slider = $("ava_slider");
		var ava_width = $("ava_width");
		var ava_x = $("ava_x");
		var ava_y = $("ava_y");
		// four numbers are minx, maxx, miny, maxy
		Drag.init(handle, null, 0, 134, 0, 0);
		Drag.init(drager, avatar, -100, 350, -100, 350);
		var start_w = avatar.width;
		var start_h = avatar.height;
		var ratio = (start_h / start_w);
		var new_h;
		var new_w;
		if (ratio > 1) {
			new_w = 80;
			new_h = (80*start_h)/start_w;
		} else {
			new_h = 80;
			new_w = (80*start_w)/start_h;
		}
		// these need to be set after we init
		avatar.style.top = '100px';
		avatar.style.left = '100px';
		avatar.style.width = new_w + 'px';
		avatar.style.height = new_h + 'px';
		avatar.style.margin = '-' + (new_h / 2) + 'px 0 0 -' + (new_w / 2) + 'px';
		handle.style.margin = '3px 0 0 20px';
		avatar.onDrag = function(x, y) {
			ava_x.value = x;
			ava_y.value = y;
		}
		handle.onDrag = function(x, y) {
			var n_width = (new_w + (x * 2));
			var n_height = (new_h + ((x * 2) * ratio));			
			avatar.style.width = n_width + 'px';
			avatar.style.height = n_height+ 'px';
			ava_width.value = n_width;	
			avatar.style.margin = '-' + (n_height / 2) + 'px 0 0 -' + (n_width / 2) + 'px';
		}
	}
}
Event.observe(window,'load',setupAva, false);
// ]]>
</script>

If this isn’t exactly crystal clear, I can explain. If you’re new to prototype, $() is the same as doucment.getElementByID() (at least for our purposes).

We need to initialize two draggable elements, one is our slider for zooming and the other is our avatar itself. We initialize the draggers using Drag.init(). We specify what to drag, if another element should be used as a handle and then the range of motion in xy coordinates. In the second call we use that #dragger to move around the image in this manner.

Drag.init(handle, null, 0, 134, 0, 0);
Drag.init(drager, avatar, -100, 350, -100, 350);

We want to initialize the the size and placement of the avatar. We do that using maths. First we want it in our 80x80 pixel box. So it should be roughly 80x80. I’ve set the math up so that the smallest side is 80 pixels (there’s reasons for doing this the other way around).

	if (ratio > 1) {
		new_w = 80;
		new_h = (80*start_h)/start_w;
	} else {
		new_h = 80;
		new_w = (80*start_w)/start_h;
	}

We then place the avatar element. We initialize it to be in the center of the screen (top: 100px;left:100px) and then nudge the image using margins.

	avatar.style.top = '100px';
	avatar.style.left = '100px';
	avatar.style.width = new_w + 'px';
	avatar.style.height = new_h + 'px';
	avatar.style.margin = '-' + (new_h / 2) + 'px 0 0 -' + (new_w / 2) + 'px';

We also use margins to place the handle.

	handle.style.margin = '3px 0 0 20px';

#ava_x and #ava_y tell us where the center of the avatar is. So when the avatar is moved we need to set these again:

	avatar.onDrag = function(x, y) {
		ava_x.value = x;
		ava_y.value = y;
	}

That was easy. Slighly more complicated is the zoomer function. We are basically adjusting the width and the height proportionately based on roughly where the slider is. Note that we’re still using that ratio variable that we calculated earlier. We basically take the new x-coordinate of the handle and allow our image to get just slightly larger than the #ava_image container.

	handle.onDrag = function(x, y) {
		var n_width = (new_w + (x * 2));
		var n_height = (new_h + ((x * 2) * ratio));			
		avatar.style.width = n_width + 'px';
		avatar.style.height = n_height+ 'px';
		ava_width.value = n_width;	
		avatar.style.margin = '-' + (n_height / 2) + 'px 0 0 -' + (n_width / 2) + 'px';
	}

We want to load initialize the slider right away when the page loads: Event.observe(window,'load',setupAva, false);

Not terribly hard or complicated. Once these elements are all in place you have a working functioning slider. It returns the x and y coordinates of the center of the image with respect to our 200x200 pixel #ava_image. It also tells us the new width of our image. We feed this information into a new script and out should pop a new image which matches exactly what we see in our GUI.

Processing the crop

Initially I was frustrated with the data that was being sent. I knew the center of the image in relation to this 200x200 pixel canvas and its width… but what could I do with that. Well I could just recreate what I saw in the GUI. I needed to create a 200x200 pixel image first, place my original avatar resized (and resampled) at the precise coordinates and then cut out the center most 80x80 pixels to become the final avatar image.

If you note in our template above for cropSuccess.php we submit our form back to the crop action. Let’s look at the action:

public function executeCrop()
{
	if ($this->getRequestParameter('file')&&$this->getRequestParameter('width')) {			// we are saving our cropped image
		// Load the original avatar into a GD image so we can manipulate it with GD
		$o_filename = $this->getRequestParameter('file');  // we'll use this to find the file on our system
		$o_filename = sfConfig::get('sf_root_dir').'/web' . $o_filename;
		$o_im = @imagecreatetruecolor(80, 80) or die("Cannot Initialize new GD image stream");
		$o_imagetype = exif_imagetype($o_filename); // is this gif/jpeg/png
	
		// appropriately create the GD image
		switch ($o_imagetype) {
			case 1: // gif
				$o_im = imagecreatefromgif($o_filename);
				break;
			case 2: // jpeg
				$o_im = imagecreatefromjpeg($o_filename);	
				break;
			case 3: // png
				$o_im = imagecreatefrompng($o_filename);
				break;
		}
		
		// Let's create our canvas
		$im = @imagecreatetruecolor(200, 200) or die("Cannot Initialize new GD image stream");
		imagecolortransparent ( $im, 127 ); // set the transparency color to 127
		imagefilledrectangle( $im, 0, 0, 200, 200, 127 ); // fill the canvas with a transparent rectangle
	
		// let's get the new dimension for our image
	
		$new_width = $this->getRequestParameter('width');
		$o_width = imageSX($o_im);
		$o_height = imageSY($o_im);
		
		$new_height = $o_height/$o_width * $new_width;
	
		// we place the image at the xy coordinate and then shift it so that the image is now centered at the xy coordinate
		$x = $this->getRequestParameter('x') - $new_width/2;
		$y = $this->getRequestParameter('y') - $new_height/2;
		
		// copy the original image resized and resampled onto the canvas
		imagecopyresampled($im,$o_im,$x,$y,0,0,$new_width,$new_height,$o_width,$o_height); 
		imagedestroy($o_im);
		
		// $final will be our final image, we will chop $im and take out the 80x80 center
		$final = @imagecreatetruecolor(80, 80) or die("Cannot Initialize new GD image stream");
		imagecolortransparent ( $final, 127 ); // maintain transparency
	
		//copy the center of our original image and store it here
		imagecopyresampled ( $final, $im, 0, 0, 60, 60, 80, 80, 80, 80 );
		imagedestroy($im);
	
		//save our new user pic
		$p = new Userpic();
		$p->setUser($this->getUser()->getUser());
		$p->setGD2($final);
		$p->save();
		imagedestroy($final);
		$this->userpic = $p;
		return "Finished";
	}


	$this->getResponse()->addJavascript("dom-drag");
	$this->getResponse()->addJavascript('/sf/js/prototype/prototype');
	$this->getResponse()->addJavascript('/sf/js/prototype/effects');
	$this->image = '/images/userpics/originals/' . $this->getRequestParameter('file');
}

It’s doing exactly what the paragraph above explains when the image dimensions are given. The code is well commented so it should be easy enough to follow.

GD image functions in PHP are fairly robust and can help you do a lot of tricks with image data. Note the code to save the image, we’ll cover it in detail soon.

The Model

$p = new Userpic();
$p->setUser($this->getUser()->getUser());
$p->setGD2($final);
$p->save();

First some clarification the second line. myUser::getUser() gets the User object associated with the currently logged in user. The third line, however, is where the magic happens. Before we look at it, let’s have a quick look at our model:

userpic:
 _attributes: { phpName: Userpic }
 id:
 user_id:
 image: blob
 thumb: blob
 created_at:
 updated_at:

We have an image attribute and a thumb property to our Userpic object. This is where we store PNG versions of each icon and their 16x16 thumbnails respectively. We do this in Userpic::setGD2():

public function setGD2($gd2_image)
{
	//convert to PNG
	ob_start();
	imagepng($gd2_image);
	$png = ob_get_clean();
	//save 16x16
	$gd2_tn = @imagecreatetruecolor(16, 16) or die("Cannot Initialize new GD image stream");
	imagealphablending( $gd2_tn, true );
	imagecolortransparent ( $gd2_tn, 127 );
	
	imagecopyresampled ( $gd2_tn, $gd2_image, 0, 0, 0, 0, 16, 16, 80, 80 );
	ob_start();
	imagepng($gd2_tn);
	$tn = ob_get_clean();
	
	$this->setImage($png);
	$this->setThumb($tn);
}

We capture the output of the full size PNG, then we scale it again and capture the output of the thumbnail and set them.

Conclusion

When it comes to web apps, having a relatively simple GUI for people to resize images can go a long way in terms of adoption rate of avatars and custom user pictures by non technical users.

Enjoy, and if you found this useful (or better implemented it) let me know.

Read full post
Using Zend Search Lucene in a symfony app

[tags]zend, search, lucene, zend search lucene, zsl, symfony,php[/tags]

Read full post
Digg-style AJAX comment editing in PHP/symfony

Digg“-style anything can be pretty slick. The AJAX-interactions on that site make it very fun to use. It’s styles have been copied everywhere, and are definitely worth copying. The latest feature that had caught my eye was the ability to edit your comments for a set time after posting them. Of course, it wasn’t just the ability to edit comments, it was AJAX too and it has a timer.

This is totally something I could use on a restaurant review site. So I started on this project. It’s pretty straight forward. For all of your posted comments you check if the owner of them is viewing them within 3 minutes of posting the commen. 3 minutes is usually enough time to notice you made a typo, but if you disagree I’ll leave it to you to figure out how to adjust the code.

For example, I make a comment, realize I spelled something wrong and then I can click on my comment to edit it. Of course using AJAX means this all happens without having to reload the web page. Therefore the edits are seemingly quick. So let’s add it to any web site.

In Place Forms

First and foremost, the ability to edit a comment means you have a form that you can use to edit and submit your changes. But rather than deal with creating a boring unAJAXy form, we’ll enlist the help of script.aculo.us.

First, each comment is rendered using the following HTML and PHP:

<div class="review_block" id="comment_<?php echo $comment->getId() ?>">  
	<p class="author"><?php echo link_to_user($comment->getUser()) ?> - <?php echo $comment->getCreatedAt('%d %B %Y') ?></p>
	<div class="review_text" id="review_text_<?php echo $comment->getId()?>"><?php echo $comment->getHtmlNote() ?></div>
</div>

Note that this div and it’s child div have unique ids that we can refer back to (comment_n and review_text_n where n is the id of the comment). We can use this to interact with the DOM via JavaScript. What we do is for each comment, we check if it is owned by the current visitor and if it’s within our prescribed 3 minute window. We can do that with some simple PHP:

<?php if ($comment->getUser() && $comment->getUserId() == $sf_user->getId() && time() < 181 + $comment->getCreatedAt(null) ): ?>
	<script type="text/javascript">
	//<![CDATA[
		makeEditable('<?php echo $comment->getId() ?>', "<?php echo url_for($module . '/save?id=' . $comment->getId()) ?>", "<?php echo url_for('restaurantnote/show?id=' . $comment->getId() . '&mode=raw') ?>", <?php echo 181-(time() - $comment->getCreatedAt(null)) ?>);
	//]]></script>
<?php endif ?>	

As you can see we run the makeEditable() function for each applicable comment. As you can guess, makeEditable() makes a comment editable. For parameters it takes the comment’s id (so it can refer to it in the DOM and other background scripts). It also takes as an argument the “save” URL as well as a URL from which it can load the raw comment. The last argument is for the timer.

Here is our function:

var editor;
var pe;
makeEditable = function(id, url, textUrl, time) {
	var div = $("review_text_" + id);
	
	pe = new PeriodicalExecuter(function() { updateTime(id); }, 1);
	
	Element.addClassName($('comment_' + id), 'editable');
	new Insertion.Bottom(div, '<div class="edit_control" id="edit_control_'+id+'">Edit Comment (<span id="time_'+id+'">'+time+' seconds</span>)</div>');
	
	editor = new Ajax.InPlaceEditor(div, url, { externalControl: 'edit_control_'+id, rows:6, okText: 'Save', cancelText: 'Cancel', 
	loadTextURL: textUrl, onComplete: function() { makeUneditable(id) } });
}

It does a couple things. It runs a PeriodicalExecuter to run the updateTime function which updates our countdown timer. It adds a CSS class to our comment div. It adds a control button to edit a comment. Lastly it uses the script.aculo.us Ajax.InPlaceEditor to do most of the magic. The hard part is done.

Periodic Execution Timer

So the updateTime function is reasonably simple. It finds the time written out in the DOM and decrements it by 1 second each second. Once it hits zero seconds it disables itself and the ability to edit the block. Let’s take a look:

updateTime = function(id) {
  var div = $("time_"+id);
  if (div) {
    var time =  parseInt(div.innerHTML) - 1;
    div.innerHTML = time;
  }
  if (time < 1) {
    pe.stop();
    var editLink = $('edit_control_'+id);
    if (Element.visible(editLink)) {
      makeUneditable(id);
      editLink.parentNode.removeChild(editLink);
    }
  }
}

Call backs

We’ll need a few call backs for the editor to work properly. Since many content pieces are converted from something else to HTML and not directly written in HTML we’ll need a callback that will load our text. We’ll also need a callback which will save our text (and then display it).

Load Text

The first call back we can see is referenced in the makeEditable() function. In our example it’s:

url_for('restaurantnote/show?id=' . $comment->getId() . '&mode=raw');

Which is a symfony route to the restaurantnote module and the show action with an argument mode=raw. Let’s take a look at this action:

public function executeShow ()
{
	$this->restaurant_note = RestaurantNotePeer::retrieveByPk($this->getRequestParameter('id'));
	$this->forward404Unless($this->restaurant_note instanceof RestaurantNote);
}

All this does is load the text (in our case the [markdown] formatting) into a template.

Save Text

The save text url in our example is:

url_for('restaurantnote/save?id=' . $comment->getId());

Using the Ajax.InPlaceEditor the value of the text-area is saved to the value POST variable. We consume it in our action like so:

public function executeSave() 
{
	$note = RestaurantNotePeer::retrieveByPk($this->getRequestParameter('id'));
	$this->forward404Unless($note instanceof RestaurantNote);
	if ($note->getUserId() == $this->getUser()->getId()) {
		$note->setNote($this->getRequestParameter('value'));
		$note->save();
	}
	$this->note = $note;
}

The note is also sent to a template that renders it, so when the save takes place, the edit form will be replaced with the new text.

Conclusion

As you can see with some script.aculo.us and symfony, it’s fairly easy to mimic “Digg-style” in-place comment editing. You can test out a real example by visiting reviewsby.us.

Read full post
How to %^&* yourself over with giant to-do lists

Q: How do you eat an elephant?
A: One bite at a time.

There’s a problem with that adage. Your body can only eat so much over a given time. The elephant will probably spoil before you make a significant dent in it.

This is no different than someone who tries to tackle an impossible to-do list. Imagine, week after week, eating and eating at this elephant. You’re digesting quite a bit, but there’s so much left. You might just be inclined to give up and quit. You’ll feel like you accomplished nothing (of value), even though you really did quite a lot of work.

No deadlines

The biggest mistake you can have with a to-do list is omitting a “due date” or deadline. If you’re to-do list looks like…:

* Write Novel
* Shovel Snow
* Cut up elephant
* Train for triathlon

…then there’s no concept of when these items need to get done. That means all these items are in the forefront at once.. or could be. A good start or end date will properly prioritize these:

* Shovel Snow - 11/1
* Cut up elephant - 11/23
* Write Novel - 12/1-3/1
* Train for triathlon - 4/1

So now, the novel is going to be written in winter. You’re going to shovel snow on the first day of November. You’ll cut up the elephant just before Thanksgiving. And you don’t need to train for the triathlon until early spring.

The deadlines serve multiple purposes. The first is, you know roughly when to do what. You shovel snow next week. You write the novel in winter. You train in spring. The other important purpose is you know when to stop. A “deadline” means that line item or project or whatever has a “drop dead” date. That means you don’t need to work on it any more. For example, if you don’t finish that novel, you don’t need to let it linger. Of course, if an item is really important and salvageable you can re assign a due date.

Too many items at once

Another related setup for disaster is having too many items at once. If you go to a buffet with the intent of eating the proprietor out of house and home, the best bet isn’t to shove everything you intend to eat all at once on a plate. No, of course it’ll look comical and you’ll never finish. At least, you won’t finish it before a lot of the food gets cold. Go up multiple times.

If you’re master to-do list is too large, it can be overwhelming. Even if you get done with what’s due now, just looking at items in the future will not only distract you, but take away from any sense of accomplishment you might get. It might help to just start your day with a small to-do list written on a piece of paper, or maybe in another file. Just look at that list until you finish. You don’t need to think or plan for the future except at the very beginning of your day, and maybe at the end of the day once you’ve finished today’s items.

Too detailed

The last disaster area is details. Too many details is when it takes more time to write down what you’re going to do than to just do it. It’s at this point where you’re writing things down just to cross them off… and as satisfying as it might be at the time, over the long run you’re not accomplishing very much.

My somewhat working system

Currently my working system takes a lot of this into account. I have one master todo list which I look at daily. I only focus on the top elements that are due in the near term, unless I have nothing due soon. Once I read that, I make a small list of things to do for the day or next block of hours. Once I’m through those (or once it’s a new day) I start over.

The whole point of a to-do list is to have things that you’ll do. Don’t make a large list if it’s undoable. It’ll wear you down and prevent you from doing the few things you can do. Start small and you can go far.

Read full post
Usability for Nerds: Traversing URLs

A recent peeve of mine is URLs that you can’t manually traverse. Let me explain. Let’s say you visit http://reviewsby.us/restaurant/cheesecake-factory. You should manually be able to remove cheesecake-factory and see an index page of restaurants at http://reviewsby.us/restaurant/. It makes logical sense for that page to be something that would enable you to find more restaurants.

This is a throwback to static web sites, that consisted of directories and files. If you accessed a file by its name, you would see the contents of the file (possibly filtered by the server). If you accessed a directory, you would see an index of files. In the world of web apps, however, URLs are made up.
Web apps where these gaps are missing can be especially frustrating when you use your browser’s history. Often I reference the symfony api. The URL is http://www.symfony-project.com/api/symfony.html. All the other URLs in the API listing begin with http://www.symfony-project.com/api/, so you could assume that http://www.symfony-project.com/api/ is an index page.

URL autocomplete

It’s not the index page (http://symfony-project.com/api/symfony.html is). If you googled for sfConfig and got to http://symfony-project.com/api/symfony/config/sfConfig.html and didn’t feel like figuring out the navigation structure… or let’s say at a later date you’re using your browser’s URL auto-complete feature, you will get a 404 Error1.

Web applications can try to mirror directory indexes with pretty URLs, but often have a few gaps as every URL (or level of URL) needs to be designated in the app. Its a good idea to fill those in as it is another way to navigate a web site.


  1. Not to pick on the wonderful symfony development team, but I truly do see this a lot on their site. I'm sure they'll setup a redirect or something to fix this. Or probably call me out on the fact that many of my sites violate this principle.

Read full post
Do you like see-food? See? Food.

Brownie Sundae

The nice thing about some restaurants is they pay extra special attention to presentation. That way if the food isn’t to your taste, maybe it is at least pleasing to your eyes. We know that just a list of each dish and comments about said dish served at a restaurant isn’t going to fly. But if we spruced it up with member-submitted pictures… well… now our eyes have something to see other than text and rating stars.

Now, who wants a sundae in a boat!

Restaurant Reviews By Us.

Read full post
How to remove file extensions from URLs

URLs should be treated as prime real estate. Their primary purpose is to locate resources on the Internet. So for a web developer, it makes sense to make things as user-friendly as possible. One effort is to remove the extensions from files. I don’t mean things like .html or .pdf, as those give you an idea that you’re reading a page of content or a PDF document. I meant things like .php or .asp or .pl, etc. These are unnecessary items that just clutter the location bar on most browsers.

There are two ways to do this. The easy way which just looks at a request, if the requested filename doesn’t exist, then it looks for the filename with a .php (or .asp or whatever) extension. In an .htaccess file:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ $1.php [L,QSA]

Now if you go to: http://domain/about the server will interpret it as if you went to http://domain/about.php.

Makes sense, but if we’re already breaking the relation between URL and filename, we may as well break it intelligently. Change that .htaccess file:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

Now if you go to: http://domain/about the server will interpret it as if you went to http://domain/index.php?q=about. How is this useful? Well now index.php is always called, so it can do anything common to all pages (which might be nothing) and do something based on the $_GET['q'] variable.

For example:

require_once('html_functions.php');
switch ($_GET['q']) {
	case 'about':
		echo myhead('about page');
		break;
	default:
		echo myhead('home page')
		break;
}
include($_GET['q'] . '.php');
echo myfoot();

We’re loading a hypothetical library html_functions.php which contains some simple functions (myhead() and myfoot()) that print out a simple header or footer for this site. The switch statement dynamically sets the <head/>. After the switch we include a file based on the query string. In our case it will still pull up about.php. Granted, this is not what I use personally, but it’s the general idea behind how symfony works.

Why?

So why go through all this nonsense? Extensions for the most part don’t mean much to an end user. Sure, jpg, png or gif mean images and html mean web page and pdf means the file is a PDF document. Dynamic pages, however usually come from cgi, php, pl , asp pages or some other 3 or 4 letter extension that the server uses as a hint to determine how to parse, but the output is usually html. Servers are smart though. They don’t need hints, and the above code eliminates the need to reveal so explicitly just how a page is delivered. Take our restaurant review site, for the most part you can’t tell that it’s done in php. In fact all the URLs are “clean” and somewhat logical. The benefit of having clean simple URLs is if we decide to change from PHP to ASP for example, we won’t need to change our URLs.

Read full post
Inverting color codes in Textmate

I deal a lot with hex color codes in CSS. One thing I occasionally need to do is invert color codes. Normally this is something I could Google for, but I wanted a solution that didn’t requiring constant reference.

My favorite text editor, Textmate, has a powerful automation system. I can write mini scripts in whatever language suitable and take advantage of the power of Unix shell scripting to execute them. From Googling, I learned enough ruby to learn that this:

printf("#%06X", 0xFFFFFF - STDIN.gets.gsub(/^#/,"").hex )

Will invert a hex color from standard input. What it’s doing is fairly simple. It’s using printf to print a formatted string. %06X means it should zero-fill the resulting string with up to six zeros, the same way a hex color string is (e.g. we write 0000FF and not FF to mean ‘blue’). The rest is simple subtraction. We take FFFFFF, the hex code for white, and subtract the input from STDIN and arrive at the inverse of what we started. Now to add this to TextMate we open Automation|Run Command|Edit Commands... and create a new command:

echo $TM_SELECTED_TEXT |ruby -e 'printf("#%06X", 0xFFFFFF - STDIN.gets.gsub(/^#/,"").hex )' 

This echo’s whatever is selected and pipes it to the ruby script. We set the command to input selected text and replace the selected text on output. Furthermore we can bind it to a keystroke. I chose Control-Alt-I, as it is unused on my system.

Voila, I can highlight any hex code and instantly invert it.

To keep this on one line, I neglected a few friendly features. One is interpreting 3-digit hex colors (e.g. #ccc), and the other is knowing whether or not to place the # in the result. If you can come up with an elegant solution, please post it below. Otherwise I hope this helps.

Read full post
Safari Fixes

Safari interprets /* */s differently than FireFox or IE. FF and IE will ignore a unmatched /* or */, whereas Safari will ignore parts of code if there’s a lone */. Once I found that out, I was able to get the list items that are used throughout the site to render properly.

Read full post
Going international... kinda

Some of the first non-Minnesotan restaurants to show up were Flying Dog, Bangalore and Konstam… all of them outside the US. Wasn’t expecting that… but then again, I wasn’t really surprised.

I finally updated our location tables to account for different countries. Currently it’ll only plot what it can Geocode, and relies exclusively on Yahoo! for GeoCoding.

I’ve only tested this with a Canadian restaurant. Hopefully it’ll work elsewhere soon. If anybody plans on adding any non-US, non-Canadian restaurants, let me know if you can figure out how to GeoCode things properly.

Also, I’m pleased as punch that the map on the homepage shows three states as having recent restaurants. Rock on!

Read full post
Going international... kinda

Some of the first non-Minnesotan restaurants to show up were Flying Dog, Bangalore and Konstam… all of them outside the US. Wasn’t expecting that… but then again, I wasn’t really surprised.

I finally updated our location tables to account for different countries. Currently it’ll only plot what it can Geocode, and relies exclusively on Yahoo! for GeoCoding.

I’ve only tested this with a Canadian restaurant. Hopefully it’ll work elsewhere soon. If anybody plans on adding any non-US, non-Canadian restaurants, let me know if you can figure out how to GeoCode things properly.

Also, I’m pleased as punch that the map on the homepage shows three states as having recent restaurants. Rock on!

Read full post
Dynamic Linking to Syndication Feeds with symfony

Adding a statically-linked syndication feed, a feed that is the same no matter where on the site you are, is a cinch with symfony, but what about dynamically linked syndication feeds? Let’s say we’re building the latest and greatest Web 2.0 app, there’s going to be hundreds of RSS feeds, not just the most recent items. We’ll want the latest comments to a post, the favorite things of a website member and it all has to be feed enabled. Sure, we can slap a link to the RSS feed and call it a day, but let’s go a step further and stick it in the <head/> area as well. That way when someone clicks on the RSS icon in their browser, or adds a web page to Bloglines those extra feeds can be found.

Expanding your head

A typical layout.php for a symfony app will have a <head/> section like this:

<head>
	<?php echo include_http_metas() ?>
	<?php echo include_metas() ?>
	<?php echo include_title() ?>
	<?php echo auto_discovery_link_tag('rss', 'feed/latest')?> 	
	<?php echo auto_discovery_link_tag('rss', '@feed_latest_georss', 
	array('title' => 'Latest Restaurants\' Locations (GeoRSS)' ))?> 	
	<?php echo include_feeds() ?><!-- this is the custom feed includer -->
	<link rel="shortcut icon" href="/favicon.ico" />
</head>

Since this is in the reviewsby.us layout.php, the latest feed and the latest GeoRSS feed (which we developed in this article) will show up on every page. So for example, if you use FireFox, you can subscribe to either link when you click on the orange feed icon (feed) in the URL bar no matter where you are in the web-application.

To expand this to allow for multiple feeds, we need to include <?php echo include_feeds() ?> (before or after the auto_discovery_link_tag calls makes the most sense).

Making the Feed Helper

Let’s created a FeedHelper.php to put the include_feeds() function (don’t forget to add use_helper('Feed') to your layout.php).

The function looks like this:

function include_feeds()
{
	$type = 'rss';
	$already_seen = array();
	foreach (sfContext::getInstance()->getRequest()->getAttributeHolder()->getAll('helper/asset/auto/feed') as $files)
	{
		if (!is_array($files))
		{
			$files = array($files);
		}
		foreach ($files as $file)
		{
			if (isset($already_seen[$file])) continue;
			$already_seen[$file] = 1;
			echo tag('link', array('rel' => 'alternate', 'type' => 'application/'.$type.'+xml', 'title' => ucfirst($type), 'href' => url_for($file, true)));
		}
	}
}

The function is doing what the deprecated include_javascripts and include_stylesheets functions did, just with syndication feeds. Also note, I stuck to just using RSS feeds. This function can no doubt be extended to Atom or other feed types, but for my purposes it was unnecessary1.

Dynamically setting the feeds

In the reviewsby.us site, the menu items are tagged. There’s tags for chicken, indian and bread for example. Each of them are to have an associated GeoRSS feed as described in a previous tutorial. I built our tagging system similar to Askeet. So in our tag module I created a function in the corresponding actions.class.php:

public function addFeed($feed)
{
	$this->getRequest()->setAttribute($feed, $feed, 'helper/asset/auto/feed');
}

This sets the attribute that include_feeds() pulls from. Here $feed is simply the route to our feed. So in our executeShow() I just make a call to $this->addFeed('@feed_tag_georss?tag=' . $tag). We’re done.

We can now go to any of our tagged pages. Let’s try chicken and see that we can subscribe to a GeoRSS feed of restaurants serving dishes tagged as chicken.

Slight problem. The title attribute of the generated link tags are always Rss. That can be mildly unusable.

Throwing feed titles into the mix

Let’s change our addFeed() to allow for a second parameter, a title and have it store both the route and the title in the request attribute:

public function addFeed($feed, $title = null)
{
	$feedArray = array('url' => $feed, 'title' => $title);
	$this->getRequest()->setAttribute($feed, $feedArray, 'helper/asset/auto/feed');
}

We’ll also need to adapt the include_feeds to appropriately accommodate associative arrays:

function include_feeds()
{
	$type = 'rss';
	$already_seen = array();
	foreach (sfContext::getInstance()->getRequest()->getAttributeHolder()->getAll('helper/asset/auto/feed') as $feeds)
	{
		if (!is_array($feeds) || is_associative($feeds))
		{
			$feeds = array($feeds);
		}

		foreach ($feeds as $feed)
		{
			if (is_array($feed)) {
				$file = $feed['url'];
				$title = empty($feed['title']) ? $type : $feed['title'];
			} else {
				$file = $feed;
				$title = $type;
			}

			if (isset($already_seen[$file])) continue;

			$already_seen[$file] = 1;
			echo tag('link', array('rel' => 'alternate', 'type' => 'application/'.$type.'+xml', 'title' => $title, 'href' => url_for($file, true)));
		}
	}
}

Note, there’s a function is_associative(). It’s a custom function that we can place in another helper:

function is_associative($array)
{
  if (!is_array($array) || empty($array)) return false;
  $keys = array_keys($array);
  return array_keys($keys) !== $keys;
}

It’s a clever way of determining if a function is an associative array or not.

Conclusion

It looks like our GeoRSS feeds are on all our tag pages. Now we can take our favorite items labeled as Indian food and easily add the URL to a service like Bloglines and have it keep us up to date on new Indian dishes. This was simple, especially when much of the work was taken care of by the framework.


  1. Most clients support RSS so unless there is a compelling need to use Atom or another format, then keeping it down to one choice is always your best bet.

Read full post
Comment editing in reviewsBy.us

Something that’s been heavily requested for reviewsBy.us is the ability to either preview or somehow edit comments after they are posted. I took a cue from digg and added some comment editing.

Since the last time we updated, the RSS feeds work better too thanks to a patch to the symfony. Also, the basis for a user profile is in place.

Read full post
Working Backwards as a Way to Prioritize Tasks

I handle multiple projects at once, and sometimes I get lost and forget what needs to get done. The biggest day of my life was a few weeks ago. I had a milesstone for a major project due for my 9-5 in this month. And I also have this blog, and a community-run restaurant review site to take care of. So figuring out what needs to happen and by when is extremely beneficial.

Sometimes a strategy that works to complete tasks is working backwards from your goal. So the pre-requisite here is to have a well-defined goal. Even though this is a programming blog, let’s take my wedding for an example. The end goal was that the ceremony and reception go off well enough that Katie and I are happy (it did). That’s pretty vague, so let’s look at a single aspect of the wedding. For example, one detail is having a wedding program. 500 wedding programs needed to be printed and delivered to the temple by Saturday morning (17 June 2006). This is a more defined goal, or component of the overall goal.

Let’s break it down:

  • What we need is to Deliver the programs
    • We’ll need to fold the programs
      • but before we can do that we need to print the programs
        • we’ll need to first get paper
        • and actually we need to know what we’re printing

Easy task, didn’t need to be broken down much, but it serves our purposes.

Prioritization - Get the Minimum Done First

Working backwards on it’s own is good for giving you a list of what to do and even when to do it by. But it doesn’t necessarily tell you what order to do it by. In fact, order might not matter, but if you’re in a slump and don’t feel like doing anything, having a prescribed order can help.

Now that we know the end goal, let’s see if we can extract what the minimum is. If we follow along with our wedding program example, the minimum would be a sheet of paper with a list of events. No details, no participants, just events. That’s easy, a cut-and-paste from an email that our officiant sent us and we’re done.

By getting the minimum done, we at least have something. Sure, we might want something fancier with an elegant design and good descriptions, but even if we botch that up for some reason we can rest easily knowing that we got our bases covered.

Finish Building an Onion

Finishing out the wedding program is a snap. We need to make some estimates first. We can improve on the program as much as we wanted, but we needed to stop by Thursday so we’d have enough time to print and fold them.

Timeline:

  • Delivery: half-hour
    • Folding: a couple days (it took only a few hours, but we can only print/fold so much at once)
      • Print: a couple days (see above)
        • get paper: :15 minutes
        • making the program better: remaining time

So in the remaining time, we came up with small revisions (essentially our onion skin layers) to our minimum. For example, finding descriptions of each event as a first revision. Our next revision can be rewriting Sanskrit headings in Devanagari script. Our final revision can be plopping this all in a nice layout design in Adobe Illustrator.

All these steps are gravy. We already had a product that was satisfactory, now we’re moving from satisfactory to pretty good to excellent.

Conclusion

So, no, this isn’t just applicable to wedding activities, but it’s a great way to prioritize tasks for any project. When I develop new web sites, like reviewsby.us, I first try to figure out what my goal is: to a have a list of the things I like to eat at various restaurants. All I need to do is build a simple on-line list. Beyond that, I can add features, and web 2.0 fluff that’ll make it more fun to use, but as long as I got the core, that’s all I need to have up to have this site be useful to me. Each additional step adds more value to myself and others.

Read full post
Breaking IE 6 with links on PNG backgrounds

In IE there’s a whole slew of troubles with PNG. One such trouble is links or anchors will not work in IE if you have a PNG image that has gone through the Microsoft [AlphaImageLoader], which is the only known way to render PNGs in IE6.

The solution, involves running the image filter on a separate element, and then positioning all the links within that element in a higher z-order. This is explained in better detail in Filter Flaws.

I’ve been running into too many designers who have been sending me too many designs that require translucent layers, and alpha-transparencies (and they should be able to, even though the trend is more simple these days). This of course subjects me to pretty much any strange quirk that IE6 can dish out with PNGs.

What’s frustrating is that 5 years ago I had PNGs problems. This is the Internet… very few problems on the Internet last that long. There’s certainly been enough people complaining about this malfunction in IE for years.

The problem with these problems is there is no “right” solution. Using [AlphaImageLoader] is a hack. As we can see with laying links upon backgrounds that use the loader, it’s prone to behavior we don’t normally expect. We shouldn’t have to raise the links above an invisible layer, in order to click on them.

Unfortunately as long as the majority of our users continue to use IE 6 (or older) we’ll be stuck with this problem. If we look at adoption rates it won’t be until a year after Internet Explorer 7 is out until it’s the top dog. Even then, if Firefox is still at 10% marketshare, that leaves another 40% with IE6 (or less) and the less popular browsers. We’re still stuck with this problem for some time.

Read full post
Random Selections using Propel

Propel is a handy way to deal with ORM. Rather than figuring out the correct SQL statement to select your elements you just use a -Peer object to select it.

The one drawback is there’s no way of choosing an object at random. You can select the first element of a result set, but not a random one without some changes to your -Peer class.

The quick and dirty fix that I did is to use custom SQL to populate a propel object. It’s a rather suitable approach for more complicated selects. So here’s how we randomly select things:

$con = Propel::getConnection('propel');
$sql = 'SELECT %s.* FROM %s ORDER BY RAND()';
$sql = sprintf($sql,MyObjectPeer::TABLE_NAME,MyObjectPeer::TABLE_NAME);
$stmt = $con->createStatement();
$rs = $stmt->executeQuery($sql, ResultSet::FETCHMODE_NUM);
$objects =  MyObjectPeer::populateObjects($rs);
$object = $objects[0];

If you know you’re going to only use one object, SELECT %s.* FROM %s ORDER BY RAND() LIMIT 1 will work as well.

Read full post
Add to Bloglines from NetNewsWire

I use NetNewsWire in conjunction with Bloglines and often come across new feeds from within NetNewsWire that I’d like to add to my Bloglines (versus adding it directly to NetNewsWire).

Bloglines is my sandbox for new feeds until I deem them worthy enough to read in NetNewsWire. So here’s an AppleScript to feed (no pun intended) my RSS addiction:

or save this apple script to ~/Library/Application\ Support/NetNewsWire/Scripts/.

Read full post
Spindrop objectives

I had wanted to write about this later, but Darren at ProBlogger started this group writing project on “Blog Goals.” So I am jumping in quickly. This blog was created for two reasons, one, to document any technical things that I’ve learned, code samples, best practices, strategies, etc. as they pertain to web development and open source. The second is to serve as a site to record updates to any of my projects.

I’m still in my infancy for this blog, but I’ve seen a lot of little things that keep me optimistic.

The objectives I have for Spindrop are both internal and external. Internal goals are things that I can change myself. For example, the style of the site, linking to other places, posting more content, changing the way ads are presented, etc.

External goals depend on readers like you. I can do my best to make this site be relevant to a lot of people, but I can’t make people click on my site, comment, or any of that. I can still make goals for them, and that will subconsciously get me to position myself better.

They are both related. If I achieve my internal objectives, I’m better suited to getting external objectives done. If I get my external objectives done, it encourages my behavior of making the site better.

What am I willing to do

My “9-5” is a fairly demanding job as a lead web developer for a health and wellness website. On top of that, I’m getting married this month, so I’m very pressed for time when it comes to a side project like this. Luckily I’m very well organized, and do wake up early and spend time writing.

Writing

In an OmniOutliner file, I keep a detailed list of what I’ll be writing as well as other “to-do list” items for this site. I’ve already taken into account that I’m not going to be pushing anything useful for the week or two surrounding my wedding. Hopefully, I can still muster two or three articles this month. Some of those articles are on software choices, strategies for using propel and symfony and general work habbits.

By spending an hour (or more) a day on writing, I’m generally looking over things and making sure I did a halfway decent job. I prefer not to have typos, spelling errors, etc, I prefer to have a readable article, and I prefer to throw in images, when useful. Of course, I do this early in the morning, so there are mistakes. But, I’ve noticed a lot of heavy hit articles on del.icio.us and digg have typos too.

Non-content changes

I’m willing to tweak ad placement, and practice all the other ad-voodoo that is involved with blogging. I’m also willing to announce the site at appropriate moments, communicate with people via forums and other blogs. I’m willing to listen and execute on others advice. The best advice I’ve heard is to stick with it.

What can I get at the very least?

With all my efforts, at the very least, I know I’ll have something to show for. For one, I tend to do things over and over again. I’m a web developer, and the bulk of my blog is about how to do things I’ve done before. So if I have to make another map or migrate a blog, I can see how I did it before. Now, anything else this blog achieves is gravy.

External goals

Some of my hopes for the site are out of my control. I want what a lot of other blogs want: traffic, revenue and community. Additionally I want traffic to be sent to my other sites, like the reviewsby.us site so that they can share the success.

Traffic

The most important goal for me is traffic. I want this site to get a lot of traffic. For the last two weeks, I’ve averaged 112/users a day. Of course, part of that is due to a spike because of last Friday’s article on Editing CSS from Firefox which had an anomalous amount of hits due to getting on reddit, del.icio.us popular, digg.1

Traffic is important because I without it I can’t expect to have revenue or community. There’s also a sense of validation that you have something useful to say, and sites like del.icio.us make you feel that not only was it useful, it’s worth hanging on to.

Revenue

I’d like to earn enough to actually make blogging make a significant change in my lifestyle. Right now it’s a hobby. A dedicated hobby, but it’s still a hobby. I love writing, I love to see myself improve, I love what I write about. I’d like to continue to do that full time. I’m sure with some tweaks to adsense and some other forms of advertising, I can kick that up. But content, which attracts traffic, will be the biggest driver of them all. My less-than-“adsense optimized” restaurant review site generates a lot more money (seriously, we’re talking in small terms, like the cost of an iced tea), but it has a lot of content and a lot of useful ads that get generated from adsense.

Last month (May 2006) I made just shy over $2.00 from the web site. That’s not much, but it’s better than $0. I’d like to make $4 in a given month. On one hand it’s a doubling of earnings, but on another, it’s not very much money. I’m not expecting to double the earnings this month, although I might easily do that, but I am expecting to hit that $4 mark eventually. It’s an easily obtainable goal, but for me easy goals are good. I feel just as good when I hit them, as I would if I got a raise or a bonus at my “9 to 5.”

Community

I’d like for there to be some level of interaction with me. I don’t mean community in the LiveJournal sense of the word, I just want some interaction with me the poster, and my commenters. The last week or two has seen a few “real comments,” which is promising. I hope that trend continues over time. It’s an opportunity to improve myself if I get good feedback, and it’s an opportunity to further express or clarify a point. I also like to help people out.

Linking out to my other sites

Lastly, I do want this site to assist my other sites. With a decent amount of traffic, this site can be a great link in to my other sites, and bring them up to speed.

Conclusion

Overall, I’m happy where I am, and I’m happy where I intend to go. Each week brings in a new useful content which is a nice record of how I do things, and proves some degree of usefulness to others.

I hope to expand on those strengths. This body of content might be small now, but it only gets bigger. My long term goal is to be able to generate the traffic and revenue I need so I can justify dedicating more time to this project.


  1. Perhaps it's not anomalous, but if you saw my statistics you'd understand why I might think so.

Read full post
Welcome Performancing Users

Google Analytics Screenshot

Referral percentages, the blue is Performancing.com

Yesterday I took advantage of the Performancing launchpad to announce reviewsby.us to the blogging world. Jumping at the opportunity was smart, I was the second site on the launchpad and made it to the homepage. As a result, I had probably my largest day of traffic (it’s not a whole lot, but it’s definitely noticeable in these early stages).

We also got a few new users and a few new restaurants. So check it out.

Read full post
Polls

I decided to install Democracy for WordPress. This was partially inspired by Darren’s post at Problogger and for my own development efforts for the restaurant review site. Not sure if it allows you to change your vote, that would be useful for the first poll. I suppose we’ll find out.

So the first poll is deciding on the next feature to implement for reviewsby.us. I’ve seeded it with some of the top requests I’ve heard/felt. But I’d like to hear other peoples ideas. If you’re not familiar with the site, take a look and tell me what you think.

Note: For most people the poll is on the left sidebar.

Read full post
ReviewsBy.Us bugfixes

A lot of bugs have popped up recently.

Logins

The logins weren’t redirecting people to the correct place. Unfortunately the login system still needs a lot of work. I am probably going to rewrite it completely. It doesn’t consistantly remember where you are coming from or where you intend to go after logging in. I’ll be jotting down a clean system to log people in propperly.

Tags

Tags work a bit better. They follow the flickr style of tagging, which is each word is a tag, unless surrounded in double quotes. Previously they weren’t producing a lot of empty tags.

Latitude and Longitude even more precise.

A small error in the database definitions resulted in lat/longitudes of restaurants that were greater than 100 (or rather abs(x) > 100 where x is latitude or longitude were being truncated to 100 or -100. This was easily fixed so things should look just right on the maps.

Read full post
Quicksilver + TextMate = craZy delicious development environment

UPDATE:Cmd-T allows you to search for the files in your currently opened TextMate project. I learned this shortly after writing this post, but forgot to mention it. Thanks again to Tyson Tune for pointing that out.

There’s a number of tools for the OS X that help me with my productivity (and unfortunately have no equivalents on other platforms). Quicksilver, a launcher, and TextMate, a text editor work wonders and together work fairly well.

Quicksilver is a the GUI equivalent to the command line. You can launch applications or files or perform any number of operations on those files or applications. With its powerful collection of plugins you can have it do much more, for example you can take a music file and play it in iTunes within the iTunes party shuffle. Or take an image file and have it submit to flickr with a few simple keystrokes. Initially, I couldn’t get an idea of the application, other than a lot of people loved it. Now, I’m barely using it to its potential and I love it. Using a computer without it is quite a drag.

TextMate is similarly feature rich and elegant. Just using a small number of its features makes it worth its cost. All my symfony projects are written using TextMate as are my articles for this web site. It’s strength for me is its automation. Together Quicksilver and Textmate make a winning combination.

Projects in TextMate

I like the concept of “projects” in TextMate (its common to a lot of text editors). You can drag files or folders into TextMate and group them as you see fit.

Many of my projects are written using symfony, so I’ll try to keep the entire project folder in my TextMate project. Additionally I’ll keep the symfony libraries if not the entire PHP libraries referenced in the project as well. Now I have access to all of my files with relative ease. I generally create a file group in TextMate of frequently accessed files to bypass the pain going through hierarchies of folders.

If I have a TextMate project open, anytime I open a file that belongs in that project, it will open in that project window. That means if I use the mate command line utility, Finder or even Quicksilver, it’ll still open in the project window. This is useful.1

Quicksilver Catalogue

Catalogues in Quicksilver

When you open up Quicksilver and type in some letters, it searches some catalogues by default (e.g. Applications, Documents, Desktop, etc) in an attempt to figure out what the “subject” of your action to be. These catalogues are fully customizable, so it’s trivial to add the directories of your project and your libraries into Quicksilver.

New Workflow

Quicksilver finding reviewsby.us

Now that you’ve setup your project in TextMate and added the same directories to Quicksilver, you’ll have a much improved workflow. If you save your TextMate project in your Documents directory, you need only open Quicksilver (I use Command+Space as a shortcut) and type a few letters of the project (for example, I named my project file reviewsby.us for my restaurant review site).

When that’s open, I can now open any file of that project in anyway I feel necessary. Let’s say I need to open a library file, like sfFeed.class.php. I need only type in a few letters and it opens inside my project.

This process now saves me a ton of time in digging through hierarchies of folders upon folders. It’s many times quicker than Spotlight. Give it a try, there’s thousands of uses for it, this is just one way I use it.


  1. While debugging errors in a project, if PHP tells you a certain file gave you a certain error, you can highlight the filename in Safari (or any other Cocoa browser), hit Command Escape (or whatever custome keystroke you setup for sending selection to Quicksilver), hit Enter and have it show up in TextMate.

Read full post
Google Analytics

Google Analytics Screenshot

I’ve also jumped on the Google Analytics bandwagon and added a lot of the sites in “family.” If you’re familiar with Urchin, it has a similar feel to it. It almost feels a bit lighter feature-wise. There’s a few issues I will take with it:

  • When they list links to your site (e.g. /login) they don’t hyperlink it.
  • I’d like to drill down to raw data. I like to make my own correlations. Heck, I want to drill down to see if I should filter someone from the list.

Other than that, I like the potential that this will offer me. This combined with Google Sitemaps will provide powerful analysis of the site. As it stands, Katie and I should be eating at the Cheesecake Factory and non-chain restaurants as we seem to do well in the ranks and I need to write more about maps.

Read full post
More usable approach to adding restaurants

The “lazy” approach to usability is to just use a web site or application yourself and use usability heuristics. This is what I do, and it’s why I have a huge to-do list of things to fix on the site. Lately to build out the site content, I’ve been going to a lot of restaurants not listed on the site. This means I have a multi-step process after I eat:

  1. Add a new restaurant.
  2. Add a location.
  3. Add a review of the restaurant.
  4. Rate the restaurant
  5. Add all the menu items I ate.
  6. Tag the menu items.
  7. Write a review of the menu items.
  8. Rate the menu item.

For everyday users, I don’t expect 1 or 2 and only a few of 3-8, but 1 is a requisite and 2 is nice for maps and just being able to get information quickly. I did want to streamline this process, so I made a combined form that does 1,2 and 3 all in one place. That’s just six steps:

  1. Add a new restaurant.
  2. Rate the restaurant
  3. Add all the menu items I ate.
  4. Tag the menu items.
  5. Write a review of the menu items.
  6. Rate the menu item.

For most users it makes sense, since most restaurants just have one location - or if they are adding a restaurant they are only thinking of a specific location.

I also took the opportunity to do a check on restaurant names. If you attempt to enter Green Mill twice, you’ll be prompted that a restaurant with that name already exists. It’s not a very smart check, but it should serve it’s purpose.

Read full post
AJAX star rater for symfony

Francois from the symfony project beat me to the punch. I was going to post a detailed how-to on adding a star-rater to your web site (similar to the one’s I created for reviewsby.us), but for most of you this should do the trick. Unless people request it sooner, I’ll hold off on publishing the details on my star-rater for a while. It only offers a few minor differences (IMO advantages) to this snippet.

Read full post
Migrating from Drupal (4.7) to Wordpress

I helped Katie setup her new blog this weekend and decided that WordPress offers much of what I want out of this blog for a lot less effort than drupal1. I decided it might be worth my time to now while this blog is in it’s infancy to try converting from drupal to WordPress.

The way I start most of my projects is with a plan:

I’m really confident that this will be easy. I don’t even have to worry about comments or anything, since this blog is pretty new, but I can demonstrate how to take care of the.

Discovery

This post details a migration path from drupal to wordpress. Some considerations had to be made since I’m using drupal 4.7.

Implementation

Copy content

I followed most of the instructions, with some alterations from vrypan.net.

I installed WordPress and in mysql ran the following commands:

use wordpress;
delete from wp_categories;
delete from wp_posts;     
delete from wp_post2cat;
delete from wp_comments

I run my drupal site in the same database server, so the data copying was a snap. If you aren’t so fortunate, just copy the relevant drupal tables temporarily your wordpress database.

First we get the drupal categories into WordPress:

USE wordpress;

INSERT INTO 
	wp_categories (cat_ID, cat_name, category_nicename, category_description, category_parent)
SELECT term_data.tid, name, name, description, parent 
FROM drupal.term_data, drupal.term_hierarchy 
WHERE term_data.tid=term_hierarchy.tid;

Again with the posts:

INSERT INTO 
	wp_posts (id, post_date, post_content, post_title, 
	post_excerpt, post_name, post_modified)
SELECT DISTINCT
	n.nid, FROM_UNIXTIME(created), body, n.title, 
	teaser, 
	REPLACE(REPLACE(REPLACE(REPLACE(LOWER(n.title),' ', '_'),'.', '_'),',', '_'),'+', '_'),
	FROM_UNIXTIME(changed) 
FROM drupal.node n, drupal.node_revisions r
WHERE n.vid = r.vid
	AND type='story' OR type='page' ;

And the relation between posts and categories:

INSERT INTO wp_post2cat (post_id,category_id) SELECT nid,tid FROM drupal.term_node ;

And finally comments:

INSERT INTO 
	wp_comments 
	(comment_post_ID, comment_date, comment_content, comment_parent)
SELECT 
	nid, FROM_UNIXTIME(timestamp), 
	concat('',subject, '<br />', comment), thread 
FROM drupal.comments ;

I ended up moving the one static page I had into WordPress’s “pages” section manually.

Since my pages are written in Markdown, I enabled the Markdown for WordPress plugin.

URLs

Now for the real test. I needed to go through each page on my site and see if I could get to it using the same URLs. Since I had only 14 posts, I did this somewhat manually. I used drupal’s built in admin to do this from most popular to least popular. Most URLs worked fine. There were a small number that didn’t for various reasons, I used custom mod_rewrite rules to handle them.

Adjusting Templates

My drupal template was fairly clean and simple. So I adjusted the CSS for the default theme in WordPress until I got what I liked. Very minimal changes had to be made to the actual “HTML.”

Make the switch

Well, time to make the switch. In the WordPress administration, I just had to tell it that it’s now going to be located at spindrop.us. Then I moved my WordPress installation to the spindrop.us web root. It was a snap. Let me know if you have any troubles.


  1. Taxonomy, legible URLs and trackback support all seemed quite difficult to master in Drupal. In Wordpress they appeared to be available standard or with a minor change in the administration panels.
  2. This is a prime reason why URLs should be clean and make sense to the end user, not the programmer of the publishing software.

Read full post
Comment Anonymously at ReviewsBy.Us

I decided to overhaul the comment system on the reviewsby.us site. I implemented an AJAX form and allow anonymous comments. Anonymous comments is a slippery slope, but reading about friction and identity some things need to be taken into account.

The site is new. It’s going to take a while before it generates a lot of traffic and therefore members and therefore comments. But it would be nice to keep collecting information and make that collection as painless as possible. Of course the potential for spam is high, but when/if that happens there are safeguards that can be put in place. I imagine, until a decent membership base is established, the rules for who can and cannot post will probably change every now and again.

Read full post
Rate your favorite dishes

Screenshot of reviewsby.us dish ratings

If you’ve stopped by reviewsby.us this week, you’ll notice we extended ratings to dishes. On a restaurant page, blue stars are averages for things you haven’t rated yourself and orange stars are your own rating. At a glance, you can now see what you should order at any given restaurant.

Read full post
More maps, better presentation and prices

[feedback]: http://reviewsby.us/feedback [r1]: http://reviewsby.us/restaurant/tony-romas/location/mall-of-america [s1]: http://spindrop.us/2006/04/26/easy_yahoo_maps_and_georss_with_symfony [fried]: http://reviewsby.us/tag/fried [r]: http://reviewsby.us I added a lot more features to [the reviewsby.us site][r]. One thing I was [ask][feedback]ed about was adding a price field. I added that in, it could use some work, but it is a start. I also cleaned up some of the location formatting (see [Tony Roma's at the MOA][r1]) so the phone numbers are legible. The real exciting thing is maps. Applying the same principles from the [Yahoo! Map tutorial][s1], I added maps to all the tag pages. Want to know [where to find fried food][fried]? Just look at the map. I'm taking a few shortcuts now. For example most of our restaurants are located in Minneapolis, therefore the maps seem to center in on the Twin Cities area. This of course may get messy over time when more restaurants get added outside the Minneapolis area. By then, I'll have some personalization setup to narrow down on restaurants based on where the visitor is located.

Read full post
Easy Yahoo! Maps and GeoRSS with symfony

[rbu]: http://reviewsby.us/ [ymap]: http://developer.yahoo.com/maps/index.html [gmap]: http://www.google.com/apis/maps/ [ygeo]: http://developer.yahoo.com/maps/rest/V1/geocode.html [GeoRSS]: http://developer.yahoo.com/maps/georss/index.html [symfony]: http://www.symfony-project.com/ [GeoRSS][GeoRSS] is an extension of RSS that incorporates geographic data (i.e. latitude/longitude coordinates). This is useful for plotting any data that might need to be placed on a map. While building out the [reviewsby.us][rbu] map, I decided to use the [Yahoo! Maps API][ymap] versus the [Google Maps API][gmap] because I wanted to gain some familiarity with another API. It was worth trying [Yahoo!'s API][ymap]. First of all, [reviewsby.us][rbu] has addresses for restaurants and Yahoo! provides a simple [Geocoding REST][ygeo] service. This made it easy for me to convert street addresses to latitude and longitude pairs (even though this wasn't required as we'll soon see).[1] The real selling point of [Yahoo!][ymap] was the [GeoRSS] functionality. I can extend an RSS feed (which [symfony] generates quite easily) to add latitude or longitude points (or even the street address), direct my [Yahoo! map][ymap] to the feed and voila, all the locations in that feed are now on the map, and when I click on them, the RSS item is displayed. That cut down on a lot of development time.

Read full post