Mark Daggett's Blog

Innovator & Bricoleur

Reading RFID Cards With Ruby and the Mac

This weekend I fooled around with the Sparkfun “RFID Starter Kit”. I purchased it from my local technology barn for around $50.00. I am teaching myself physical computing, and projects like that can be completed in an afternoon, are a great way to learn “just enough” to keep a junior hardware hacker like myself from getting frustrated.

I was thrilled to find out that getting this board to talk to Ruby took virtually no effort. In fact, it was so simple it almost felt like I was cheating some how. Here are the steps that I followed to get the ID-12 RFID Scanner and Reader talking to Ruby.

  1. Download the most recent drivers from Future Technology Devices International: http://www.ftdichip.com/Drivers/VCP/MacOSX/FTDIUSBSerialDriver_v2_2_14.dmg The package contains two sets of drivers. Make sure that you install the version that is right for your operating system.

  2. Install the serialport gem for Ruby gem install serialport (I used version 1.0.4)

  3. Mount the scanner on top of the reader by lining up the prongs in the appropriate slot. Then plug the reader into the Mini-USB port of the mac.

  4. From the console, locate the virtual com port the drivers created when you plugged in your Mini-USB cable. :~ ls -la /dev

    Scanning the output from this command you should see the device listed similar to this: cu.usbserial-XXXXXX (where X’s represent a driver id). Mine showed up as “cu.usbserial-A900ftPb” but yours may be different.

  5. Use this sample code to print the unique RFID id stored inside the card, when someone swipes the card over the scanner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Simple example of reading from serial port to interface with the RFID reader.
require "serialport"
class RfidReader
 attr_accessor :key

 def initialize(port)
   port_str = port
   baud_rate = 9600
   data_bits = 8
   stop_bits = 1
   parity = SerialPort::NONE
   @sp = SerialPort.new(port_str, baud_rate, data_bits, stop_bits, parity)
   @key_parts = []
   @key_limit = 16 # number of slots in the RFID card.
   while true do
     main
   end
   @sp.close
 end

 def key_detected?
   @key_parts << @sp.getc
   if @key_parts.size >= @key_limit
     self.key = @key_parts.join()
     @key_parts = []
     true
   else
     false
   end
 end

 def main
   if key_detected?
     puts self.key
   end
 end
end

RfidReader.new("/dev/cu.usbserial-A900ftPb")  #may be different for you

Once you have the unique ID the possibilities become nearly limitless, you can use the card to update twitter, or turn on your lights, turn on the coffee pot, fire the nerf guns, release the hounds etc. Have fun, I know I will!

9 Secrets to Running a Successful Crowdfunding Campaign.

As many of your know, I am one of the co-founders of Pledgie. Occasionally, I get asked for advice on how to make an effective campaign on Pledgie and so I began to seriously research this topic about three months ago. Using research, expert advice and by analyzing the thousands of existing Pledgie campaigns I attempted to distill the qualities of a successful campaign into a series of tips (actually nine of them). Whether you are an individual or an organization using Pledgie to raise money, these points may help you to craft a winning message.

1. Empathy Over Sympathy

The job of your campaign message is to focus on the empathetic link between your cause and your donors. Instead of trying to elicit feelings of guilt or pity, focus on positive connections your donors share with your campaign.

Humans, like many other animals, feel empathy and sympathy for one another. Both are incredibly powerful emotions but when it comes to triggering a donor to make a commitment empathy rules the day, and here’s why. Empathy is both the ability to logically understand the experience of others, and simultaneously share a visceral emotional link. Sympathy, however is our ability to feel sorry for someone else’s misfortune, but at a more abstract emotional level. For example, a parent might feel empathy looking at a sick child, because they can imagine their own child in a similar state. In contrast someone without kids might just feel sympathy for child without feeling the greater shared circumstance.

2. Tell A Story

When crafting your campaign, tell the story of your cause. People are more likely to connect with a narrative than with facts and figures. When crafting your campaign story, ask yourself if it answers these questions: Who are you? Why is this campaign important to you? What problem does this campaign seek to solve? Why is this campaign important to the donor?

3. Be Yourself

Donors do not contribute to an idea; they contribute to a person. When describing yourself or your organization, use simple and direct language that gives a brief and complete picture of who you are, and why your campaign is a passion worth funding. The goal is to get potential donors to start seeing you as a real person and not just words and images on a webpage.

4. Don’t Forget to Ask

This seems obvious, but you’d be surprised how many campaigns never ask for funds! People often think that the goal of a Pledgie campaign is to describe the need. While that is an essential component of a good campaign, the actual goal of a Pledgie campaign is to get others to do something about your need. When crafting your campaign message you should concentrate on adding language that: Encourages potential donors to act immediately. Gives them explicit actions they can take to help your campaign.

5. Get Your Friends & Supporters Involved

Your friends and supporters are your base, they provide a stable foundation to build your outreach upon. When spreading your message through social networks like Twitter or Facebook, it is essential that your friends help make a personal appeal. As a message spreads across your social graph it can suffer an entropy in trust. This may happen, because as your message spreads the recipients are less likely to know you personally. Asking for personal appeals from others help, because people are more likely to give if someone they know is personally invested.

6. Look for collaborators not benefactors

Donors who are funding a cause want to feel like they are part of a solution, and not just performing an act of charity. Craft your message in a way that demonstrates to the donor that they are investing in the outcome, and that you will keep them up-to-date as you progress towards your goal.

7. Photo Finish

We cannot stress enough how important having a video or photo is when crafting your campaign. Even if the photo is just a portrait of yourself, simply having a photo or video associated with your campaign description can make a huge difference in perceived credibility.

8. Follow Up With Progress Reports

Donors who have donated once are likely to donate again if you ask them. Sometimes the easiest way to ask is to share your progress towards your goals. Giving periodic updates to your donor base shows them that they made a wise investment in you, and that you can be trusted with additional funds.

9. Be Grateful

When someone makes an effort to support your cause, be sure to say thank you. It gives them a sense of connection and fulfillment that can lead to future support. It is also the right thing to do!

On Jumping

Jump and the net will appear.

Seven years ago there was much handwringing around leaving my job to start my own consultancy. That is until my friend told me this quote from John Burroughs. It is a compelling illustration about faith in yourself and that the world values you. It often resurfaces for me when I am mulling over a risky proposition. The best thing about this quote is when properly applied, if the net doesn’t appear… well it won’t hurt long!

Thoughts on Requirement Gathering

Requirement documents should be a recipe not a shopping list.

This metaphor is my attempt to encapsulate what a good requirements document is. Both a shopping list and a recipe are essentially lists of ingredients in various quantities. However, the recipe also includes a desired outcome and precise descriptions of how the ingredients are meant to be used.

When evaluating a requirement document ask yourself if it gives you a clear understanding of what it is you are being asked to build, and how the independent parts work together. If it doesn’t do that then, my friend, you are reading a shopping list.

Client-side Request Caching With Javascript

Recently I was writing an enterprise data visualization application that made heavy user of interactive charts and graphs. Like most best-of-breed data-viz apps this one supported very robust filters for slicing and dicing through the dataset. Each time the user adjusted one of these filters the application made new AJAX request and idled until the results were returned.

Technically, this approach worked fine, but because the data-segmentation occurred on the server the charts felt sluggish because they were always polling or data. Additionally, the user quite frequently toggled between only a couple filters to compare the results. What should have been an experience of rapidly flipping between two views on the data was actually a belabored rendering experience. As the developer this was frustrating because they were asking for and receiving the same data over and over again.

To solve this problem, I built a very simple mechanism that affords just enough caching to persist these payload objects only while the user is viewing the page. In this way the user would be guaranteed to get a fresh copy from the server on each page load.

Essentially, I hooked my caching routine around the function that made the AJAX request for new chart data. Using this approach an AJAX request only occurred once, and all future requests pulled from the cache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Called when someone adjusts a filter
function updateChart(url, chart, key) {

    // Builds the request params needed to correctly query the server. 
    var opts = requestParamsFor(chart, key);

    // Generate a cache key based on this object
    var cacheKey = $.cache.getKey(opts);

    if ($.hh.cache.exists(cacheKey)) {

        // If the key exists then the request has happened in the past
        // use the cached result to refresh the chart.
        var result = $.cache.get(cacheKey);
        onSuccess(kind, opts, chart, code, result);
    } else {
        $.ajax({
            url: url,
            type: 'POST',
            data: opts,
            success: function(result) {

                // Since this was a new request store the results in the cache 
                // at the location specified by the cache key.
                $.cache.add(cacheKey, result);
                onSuccess(kind, opts, chart, code, result);
            }
        });
    }
}

Here is the local cache class in all it’s detail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
$.cache = (function() {
    var _cache = {};
    var _keys = [];
    var _indexOf = function(arr, obj) {
        var len = arr.length;
        for (var i = 0; i < len; i++) {
            if (arr[i] == obj) {
                return i;
            }
        }
        return - 1;
    };
    var _serialize = function(opts) {
        if ((opts).toString() === "[object Object]") {
            return $.param(opts);
        } else {
            return (opts).toString();
        }
    };
    var _remove = function(key) {
        var t;
        if ((t = _indexOf(_keys, key)) > -1) {
            _keys.splice(t, 1);
            delete _cache[key];
        }
    };
    var _removeAll = function() {
        _cache = {};
        _keys = [];
    };
    var add = function(key, obj) {
        if (_keys.indexOf(key) === -1) {
            _keys.push(key);
        }
        _cache[key] = obj;
        return $.hh.cache.get(key);
    };
    var exists = function(key) {
        return _cache.hasOwnProperty(key);
    };
    var purge = function() {
        if (arguments.length > 0) {
            _remove(arguments[0]);
        } else {
            _removeAll();
        }
        return $.extend(true, {},
        _cache);
    };
    var searchKeys = function(str) {
        var keys = [];
        var rStr;
        rStr = new RegExp('\\b' + str + '\\b', 'i');
        $.each(_keys,
        function(i, e) {
            if (e.match(rStr)) {
                keys.push(e);
            }
        });
        return keys;
    };
    var get = function(key) {
        var val;
        if (_cache[key] !== undefined) {
            if ((_cache[key]).toString() === "[object Object]") {
                val = $.extend(true, {},
                _cache[key]);
            } else {
                val = _cache[key];
            }
        }
        return val;
    };
    var getKey = function(opts) {
        return _serialize(opts);
    };
    var getKeys = function() {
        return _keys;
    };
    return {
        add: add,
        exists: exists,
        purge: purge,
        searchKeys: searchKeys,
        get: get,
        getKey: getKey,
        getKeys: getKeys
    };
}).call(this);

Here are some jasmine tests which explain more features of the cache not covered in this post, and prove that it works!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
it("should allow you to build a cache using keys",
function() {
    var obj = {
        'foo': 'bar'
    };
    expect($.cache.exists("foo=bar")).toEqual(false);
    expect($.cache.getKey(obj)).toEqual('foo=bar');
    expect($.cache.getKey('foo')).toEqual('foo');
    expect($.cache.add("foo=bar", obj)).toEqual(obj);
    expect($.cache.exists("foo=bar")).toEqual(true);
    expect($.cache.get("foo=bar")).toEqual(obj);
    expect($.cache.get("bar")).toEqual(undefined);
});

it("should allow you to empty the cache completely",
function() {
    $.cache.purge();
    expect($.cache.add("baz", 'baz')).toEqual('baz');
    expect($.cache.getKeys().length).toEqual(1);
    expect($.cache.purge()).toEqual({});
});

it("should allow you to empty the cache of just a specific record",
function() {
    $.cache.purge();
    expect($.cache.add("baz", 'baz')).toEqual('baz');
    expect($.cache.add("boff", 'ball')).toEqual('ball');
    expect($.cache.getKeys()).toEqual(['baz', 'boff']);
    expect($.cache.purge('boff')).toEqual({
        'baz': 'baz'
    });
    expect($.cache.getKeys()).toEqual(['baz']);
    expect($.cache.purge('bozz')).toEqual({
        'baz': 'baz'
    });
    expect($.cache.getKeys()).toEqual(['baz']);
});

it("should allow you to search for keys in the cache",
function() {
    $.cache.purge();
    var obj = {
        'bar': 'baz'
    };
    $.cache.add('bar=baz', obj);
    expect($.cache.getKeys().length).toEqual(1);
    expect($.cache.getKeys()).toEqual(["bar=baz"]);
    expect($.cache.searchKeys("bar")).toEqual(["bar=baz"]);
    expect($.cache.searchKeys("bar=")).toEqual(["bar=baz"]);
    expect($.cache.searchKeys("bat")).toEqual([]);
});

Rails Protip: hash.slice

Rails has hidden gems just waiting to be discovered. I will demonstrate the use of Hash.slice, which is one of the core extensions of ActiveSupport.

Here is an example of how Hash.slice can clean up a controller, take this existing code for example:

1
2
3
def index
  @users = User.paginate({ :page => params[:page].present? ? params[:page].to_i : 1, :per_page => params[:per_page].present? ? params[:per_page].to_i : 12 })
end

With Hash.slice we can shorten it to:

1
2
3
def index
  @users = User.paginate({ :page => 1, :per_page => 12 }.merge(params.slice(:page, :per_page)))
end

Hash.slice is that it is very forgiving. The method only returns the attributes if they exist. In our example we are assured all conditions will be met because the default values will only be overwritten if Hash.slice returns them.

The Minimally Viable Party

Garry and I are planning our next big project together. In the spirit of agile development and with the reality of limited funds we are ruthlessly scoping our efforts around a minimal feature set. We want to develop just enough of the product to see if we have a hit. Typically, this process is described as developing the minimum viable product (MVP).

The MVP approach targets the hardcore vocal minority that understand your offering, and are likely to give you helpful insights on how to improve it. With this in mind we began to list our potential features and aggressively cut anything that wasn’t essential.

We tried a variety of approaches to identify our MVP. Which included:

  • Sorting features in order of complexity, and identifying those with serial dependencies
  • Selecting only those features that touch the revenue line (a topic for another post)
  • Determining those features which could give us a competitive advantage over other similar products.

These thought experiments were helpful, but the focus felt very myopic, and more about cutting than pruning; like shaping a bonsai tree blindfolded. However, while mowing the lawn (where I do much of my good thinking), I came up with a new approach: “The Minimum Viable Party”.

A party seemed like a perfect metaphor for these reasons:

  • The goal of product at this stage, is to meet people, show them a good time and give them a complete experience they can give feedback on.
  • Parties are events with a specific beginnings and ends. Being the host narrows your responsibilities to just throwing a great party. If you find yourself needing to first build the venue, or starting a catering company at the same time then you are doing it wrong.
  • Parties are fun, (even Goth Emo parties); they are about doing something you love, with others looking for the same thing.
  • A complete party is more than just good food. There are many aspects that can be considered including venue, theme, duration, etc.
  • If it all goes horribly wrong you can recover. You just clean up the mess, pull the lawn chairs off the roof, get a tow company to dredge your car from the neighbors pool and go on with your life.
  • By breaking a party into smaller components you can map them onto the MVP. Now, I am not for a minute claiming that there is an absolute one-to-one mapping between party to product. However, the metaphor did allow me to consider the attributes of my product in a more objective and holistic way. For example, a decision on whether to spend money on party invitations could be construed as a marketing spend on promoting our product.

Planning a Minimal Viable Party

Here are the rules for the Minimal Viable Party thought experiment:

  • You are planning a party for people you do not know.
  • You have one week to plan and execute your party.
  • Without specifying a specific amount you should assume that funds are very limited, which should force you to make decisions on how and where to spend your money.
  • The party is not a catered meaning that much if not all the work should be done personally.

These rules lead me to a series of questions to consider which i’ve detailed below:

Q. How many guests should I invite?

A. You should invite the number of guests you can host comfortably. Everyone wants to feel special at the party, meaning you should know your limits before the inviting others.

Insight: Many people focus on the hockey stick style growth from the outset, that is a result of a good product not the goal itself. At this stage the goal is to get to know the users, and the only way to do that is to ensure there is enough of you to go around.

Q. How do I entertain people I have never met?

A. Plan a party around the type of guest you’d want to see again. If you are a geek at heart then have your party on the holodeck and don’t mind the haters.

Insight: You can’t please everyone but it’s important that you understand who you’d like as customers and friends. Ensure that your product gives them a memorable and enjoyable experience.

Q. What kind of food should I cook?

A. Be honest about your own cooking skills, anything you don’t do well you should either eliminate or buy (even if this means you have to buy all the food).

Insight: No one wants to eat bad food, a strangers will not give you an “A” for effort when eating your half-cooked hamburgers. The same is true for a poorly executed product. I continually have to fight the urge to be an everything expert. While striving to learn new things is a positive, not knowing (or ignoring) your weaknesses limits you from being effective under a deadline.

Q. How many courses should I prepare?

A. What would your ideal guest expect? Not everyone expets (or even wants) a five course meal that takes hours to eat. What they will want is for it to feel complete, and that differs from person to person.

Insight: The expression “soup to nuts” is often used when describing a project completed from beginning to end. It alludes to a complete meal that included appetizers (soup), nuts (dessert) and everything in between. If you view your features through the lens of completeness it should help you determine if a feature is needed now or can wait.

Generate Beautiful Gradients Using Javascript

I use Highcharts an excellent charting library built in Javascript for much of my data visualization and analytic work. However, one thing that has always bugged me is their collection of default series colors, which are a bit limp. Highcharts does provide an easy way to manually override their defaults with your own list of colors. Unfortunately, it’s not always possible to know in advance how many series will be supplied to the chart object. In the cases where there are more series than there are manually supplied colors the Highcharts default colors will start to leak into your charts and ruin the visual aesthetics. I wrote the gradient generator to produce a range of colors based on the parameters you supply. You simply supply start and stop colors and the number of steps in between, and you’ll be sure to have enough colors for your series.

Even though I made this generator for a need I had in highcharts, you can use it anywhere you want a uniform sequence of colors. Here is how I use it in Highcharts:

1
2
3
4
5
6
7
8
9
10
11
12
var seriesColors = gradientFactory.generate({
    from: "#0000FF",
    to: ""#FF0000"
    stops: chartConfig.series.length
})
$.each(chartConfig.series,
function(i, v) {
    seriesOptions.push({
        color: seriesColors[i]
        // other attributes ...
    });
});

Here is the code for the Gradient Factory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
gradientFactory = (function() {
    var _beginColor = {
        red: 0,
        green: 0,
        blue: 0
    };
    var _endColor = {
        red: 255,
        green: 255,
        blue: 255
    };
    var _colorStops = 24;
    var _colors = [];
    var _colorKeys = ['red', 'green', 'blue'];
    var _rgbToHex = function(r, g, b) {
        return '#' + _byteToHex(r) + _byteToHex(g) + _byteToHex(b);
    };
    var _byteToHex = function(n) {
        var hexVals = "0123456789ABCDEF";
        return String(hexVals.substr((n >> 4) & 0x0F, 1)) + hexVals.substr(n & 0x0F, 1);
    };
    var _parseColor = function(color) {
        if ((color).toString() === "[object Object]") {
            return color;
        } else {
            color = (color.charAt(0) == "#") ? color.substring(1, 7) : color;
            return {
                red: parseInt((color).substring(0, 2), 16),
                green: parseInt((color).substring(2, 4), 16),
                blue: parseInt((color).substring(4, 6), 16)
            };
        }
    };
    var _generate = function(opts) {
        var _colors = [];
        var options = opts || {};
        var diff = {
            red: 0,
            green: 0,
            blue: 0
        };
        var len = _colorKeys.length;
        var pOffset = 0;
        if (typeof(options.from) !== 'undefined') {
            _beginColor = _parseColor(options.from);
        }
        if (typeof(options.to) !== 'undefined') {
            _endColor = _parseColor(options.to);
        }
        if (typeof(options.stops) !== 'undefined') {
            _colorStops = options.stops;
        }
        _colorStops = Math.max(1, _colorStops - 1);
        for (var x = 0; x < _colorStops; x++) {
            pOffset = parseFloat(x, 10) / _colorStops;
            for (var y = 0; y < len; y++) {
                diff[_colorKeys[y]] = _endColor[_colorKeys[y]] - _beginColor[_colorKeys[y]];
                diff[_colorKeys[y]] = (diff[_colorKeys[y]] * pOffset) + _beginColor[_colorKeys[y]];
            }
            _colors.push(_rgbToHex(diff.red, diff.green, diff.blue));
        }
        _colors.push(_rgbToHex(_endColor.red, _endColor.green, _endColor.blue));
        return _colors;
    };
    return {
        generate: _generate
    };
}).call(this);

Here are a few Jasmine Specs to prove it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
describe("Gradient Generator",
function() {
  it("should generate a series of gradient colors",
  function() {
      expect($.hh.plugins.gradientFactory.generate({
          from: '#aed0ee',
          to: '#2a5980',
          stops: 2
      })).toEqual(['#AED0EE', '#2A5980']);
      expect($.hh.plugins.gradientFactory.generate({
          from: '#000000',
          to: '#999999',
          stops: 10
      })).toEqual(['#000000', '#111111', '#222222', '#333333', '#444444', '#555555', '#666666', '#777777', '#888888', '#999999']);
  });
});

A big tip of the hat to my friend jim bumgardner, who i cribbed the color conversation functions from.

Enterprise Quality Login Validation for Rails 3

In the world of enterprise software development sometimes you need to be a little more thorough with your validations than you would for other types of clients. Here is a simple yet effective login validator that passes the typical forms of penetration testing you will find in the enterprise world.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LoginFormatValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    if value.present?
      unless value.gsub(" ",'') == value
        object.errors[attribute] << (options[:message] || "cannot contain any whitespace")
      end
      if [value[0], value[-1]].any?{ |x| x == "." }
        object.errors[attribute] << (options[:message] || "cannot contain a period at the start or end")
      end
      unless value =~ /[a-zA-Z]/
        object.errors[attribute] << (options[:message] || "must contain at least one letter")
      end
      unless value =~ /[0-9]/
        object.errors[attribute] << (options[:message] || "must contain at least one number")
      end
      unless value =~ /[_\-.]/
        object.errors[attribute] << (options[:message] || "must contain at least one these special characters \"-_.\"")
      end
      unless value =~ /^[a-zA-Z0-9_\-.]+$/
        object.errors[attribute] << (options[:message] || "can only contain letters number and special these special characters \"-_.\"")
      end
    end
  end
end

You can use it this way:

1
validates :login, :presence => true, :login_format => true, :uniqueness => { :case_sensitive => false }, :length => { :maximum => 64, :minimum => 6 }

Here are some specs to prove it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
it "should only allow letters numbers and spaces in the name attribute" do
  ['foo', 'foo bar baz', 'foo 123 - bar baz'].each do |n|
    @user.name = n
    @user.save
    @user.errors[:name].should be_empty
  end
  @user.name = '<script>alert("test");</script>'
  @user.save
  @user.errors[:name].first.should == "is invalid"
end

it "should not allow you to change an email for an existing record" do
  @user.email = "test@test.com"
  @user.save
  @user.errors[:email].first.should == "cannot be changed once assigned"
end

it "should be case insenitive" do
  @user2 = Factory.build(:user)
  @user2.login = @user.login.capitalize
  @user2.save
  @user2.errors[:login].first.should == "has already been taken"
end

it "should be at least 6 characters and no more than 64 characters" do
  @user2 = Factory.build(:user, :login => 'a1_')
  @user2.save
  @user2.errors[:login].first.should == "is too short (minimum is 6 characters)"
  @user2.login = 'Pneumonoultramicroscopicsilicovolcanoconiosis1_is_a_very_long_word'
  @user2.save
  @user2.errors[:login].first.should == "is too long (maximum is 64 characters)"
end

it "should ensure the name is a mix of alpha chars (A-Z or a-z), numeric chars (0-9), and special characters (- _ .)" do
  @user2 = Factory.build(:user, :login => '1----.----1')
  @user2.save
  @user2.errors[:login].first.should == "must contain at least one letter"
  @user2.login = "a------------------a"
  @user2.save
  @user2.errors[:login].first.should == "must contain at least one number"
  @user2.login ="abc12345676789"
  @user2.save
  @user2.errors[:login].first.should == "must contain at least one these special characters \"-_.\""
end

it "should ensure the special character '.' is not used at the beginning and end" do
  @user = Factory.build(:user, :login => '.Admin_f00')
  @user.save
  @user.errors[:login].first.should == "cannot contain a period at the start or end"
  @user = Factory.build(:user, :login => 'Admin_f00.')
  @user.save
  @user.errors[:login].first.should == "cannot contain a period at the start or end"
end

it "should not allow white space or \ / \" [ ] : | < > + = ; , ? * @" do
  @user = Factory.build(:user)
  @user.login = "user _1"
  @user.save
  @user.errors[:login].first.should == "cannot contain any whitespace"
  ["\\", '/', '"', '[', ']', ':', '|', '<', '>', '+', '=', ';', ',', '?', '*', '@', "'"].each do |char|
    @user.login = "user_1" << char
    @user.save
    @user.errors[:login].first.should == "can only contain letters number and special these special characters \"-_.\""
  end
end

Simple Rails 3 Validator for Unchangeable Attributes

Here is a simple Rails validator that you can use to ensure an attribute of a model cannot be changed once it has been assigned a value.

1
2
3
4
5
6
7
8
9
10
class UnchangeableValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    if !object.new_record? && value.present?
      original = object.class.send(:where, "id = #{object.id}").select("id, #{attribute.to_s}").first
      if original.send(attribute) != value
        object.errors[attribute] << (options[:message] || "cannot be changed once assigned")
      end
    end
  end
end

You can use it this way:

1
validates :email, :unchangeable => true, :presence => true

Here are some specs to prove it works:

1
2
3
4
5
it "should not allow you to change an email for an existing record" do
  @user.email = "test@test.com"
  @user.save
  @user.errors[:email].first.should == "cannot be changed once assigned"
end