Multi-stage deployments with Capistrano
I'm seeing a recurring theme in my feed reader today - multi-stage deployments with Capistrano. It seems that those of us working in larger companies have all hit the same issue - we have some form of user acceptance, staging or other system which mirrors the production in configuration; and we want to use Capistrano to deploy to it as well as production. I know that my current company has a 5-stage process for pushing out releases (we're in financial services, and very paranoid. Whether they'd ever accept Capistrano is another issue).
So here are my lazy pointers to the discussions:
Ola Bini, who's been doing some great posts on ruby metaprogramming, calls out his need for a "production_test" environment.
Simon Harris shares some snippets of deploying to his staged server running mongrel.
Jamis Buck, the creator of Capistrano, weighed in a while ago on multiple deployment stages
Update: Michael Buffington shares his Mongrel deployment for multiple stages
Prototype based Tag Autocompletion
I've been working on a website that uses tags internally. The acts_as_taggable plugin has been great, but it only takes care of the ActiveRecord portion of tagging. One of the pieces of functionality I needed was to create an autocompleter for users to enter a list of tags on a record. Below is a pure copy-paste dump of my implementation. I'm not claiming it's good code or the right way top do things, but for those looking to use an autocompleter for a text field holding tags might find it useful. The implementation is close to that used by delicious in the tags field on posting new bookmarks.
Here's an example usage:
<label for="tags">Tags</label>
<%= text_field_tag 'tags', @tags.join(' '), :size => 80, :autocomplete => "off" %>
<div class="auto_complete" id="tags_auto_complete"></div> And the prototype-based class in javascript. You'll probably want to stick this in your application.js file if you're using Rails.
String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,'') }
Autocompleter.Tag = Class.create();
Autocompleter.Tag.prototype = Object.extend(new Autocompleter.Base(), {
lastEdit: '',
currentTag: {},
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getCurrentTag: function() {
if(this.element.value == this.lastEdit) return true // no edit
if(this.element == '') return false
this.currentTag = {}
var tagArray=this.element.value.toLowerCase().split(' '), oldArray=this.lastEdit.toLowerCase().split(' '), currentTags = [], matched=false, t,o
for (t in tagArray) {
for (o in oldArray) {
if(typeof oldArray[o] == 'undefined') { oldArray.splice(o,1); break }
if(tagArray[t] == oldArray[o]) { matched = true; oldArray.splice(o,1); break; }
}
if(!matched) currentTags[currentTags.length] = t
matched=false
}
// more than one word changed... abort
if(currentTags.length > 1) {
// hideSuggestions(); TODO Fix this!
return false;
}
this.currentTag = { text:tagArray[currentTags[0]], index:currentTags[0] }
return true
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
this.lastEdit = this.element.value;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
tagArray = this.element.value.trim().split(' ')
if (!this.getCurrentTag()) return; // We had a problem getting the current tag, just return?
tagArray[this.currentTag.index] = value;
this.element.value = tagArray.join(' ');
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
entry = entry.trim().split(' ').pop()
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
JRuby guys hired by Sun, Netbeans Ruby IDE to come?
Given the news about Sun hiring the JRuby developers, I thought I should chime my two cents in on the ongoing blog discussions.
First, and foremost, congratulations to Thomas and Charles. This is great news for their project and for Ruby in general. It goes a long way to say that the company behind Java is now supporting a project to run Ruby on the JVM. Maybe now I can give presentations on Ruby at my employer and not be chased with pitchforks and tar.
Next, I'd like to address a number of commenters out there. In particular, Cote': Hi there. There's already a project out there to make Eclipse into a Ruby IDE. I's called RDT and I'm one of the lead developers. It's also the set of plugins that those RadRails guys build on top of. Go check it out. Oh, and Tim Bray should too. He makes no mention of it, but maybe that has to do with politics...
Speaking of politics, the underlying tone behind the news is that Sun is looking to create a Ruby IDE in Netbeans. I have to say I'm a bit torn over this. It's great to see a large company want to create a full Ruby IDE and competition leads to better products for the end users, the Ruby community. But can Sun please get over itself and acknowledge Eclipse exists? It seems a bit of a waste of time for them to roll their own IDE rather than support an existing editor like RDT (or FreeRIDE, or whatever). I guess it's a bit too naive of me to think that they'd do something that didn't push their corporate agenda to some extent. Well, I guess I could always ask IBM to throw me some cash...
Tim Bray on Ruby IDEs
Tim Bray has been posting an ongoing series of articles documenting his experience in creating a Ruby based Atom protocol exerciser. His inisghts are a nice look from a newcomer to the language and he makes a good case for a number of areas where Ruby is behind the times and behind other prevailing languages.
One such case is in IDEs. I've been aware of this since I began looking at Ruby, and obviously with my work on RDT I've been trying to help out in this regard.
I would encourage Tim and other newcomers to the language to give RDT a serious try. While we're light-years away from the level of functionality found in Java support for Eclipse, we've been making some exciting progress on RDT lately.
In fact, for those who don't mind the bleeding edge, you can download our nightly builds via Eclipse's update mechanism at http://updatesite.rubypeople.org/nightly.
The latest builds now include the work that was completed by Jason in his Google Summer of Code project. So the astute among you should now notice mark occurences support for variables, and even some code completion. We're working to polish those features up and get code completion working under more conditions. Right now, it'll work easily to complete variable names in scope or methods on a declared type. There are some severe limitations as to when the method completion will work for now: it's the first method in the chain on the object and the type is able to be inferred (i.e. declared in scope). To try out the type inferrencing (and show I'm not lying!), you can use a simple example of invoking code completion on code like "1.".
I know, it's a long way from the JDT, but we're getting there. And with a pending patch to JRuby, we'll also be able to integrate the refactoring/code generation work by Mirko and company.