Page 1

LUG Villafranca Programming Course Version: 1.1 Release date: 18/03/2008

Copyright Š 2008 John Leach. All rights reserved.


Table of Contents LUG Villafranca Programming Course ............................................................................. 5 Course Outline .................................................................................................................. 5 Lessons Held ..................................................................................................................... 5 Good Tools = Good Start .............................................................................................. 6 General Language Agnostic Tools ................................................................................. 6 JavaScript Tools ................................................................................................................ 7 Ruby Tools ......................................................................................................................... 7 Ajax Tools .......................................................................................................................... 8 Recommended Reading ................................................................................................... 8 Welcome to the Course ........................................................................................................ 9 Simplifications ................................................................................................................... 9 Observations ..................................................................................................................... 9 What's Next? ................................................................................................................... 10 Simple JavaScript Concepts ............................................................................................... 11 Programming in 60 Seconds ......................................................................................... 11 Simple Expressions ........................................................................................................ 12 Simple Variables .............................................................................................................. 14 Simple Functions ............................................................................................................ 15 Conditional Expressions ............................................................................................... 16 Simple Conditional Statements ..................................................................................... 17 What's Next? ................................................................................................................... 19 Advanced JavaScript Concepts ......................................................................................... 20 Your First Aptana Studio Project ................................................................................. 20 Modifying the XHTML Document ........................................................................ 21 Adding Tags to the XHTML Document ............................................................... 21 Running the Debugger .................................................................................................. 22 Some Detective Work Required ............................................................................... 23 Other JavaScript Types .................................................................................................. 24 Arrays .......................................................................................................................... 24 Associative Arrays ...................................................................................................... 25 Regular Expressions .................................................................................................. 26 Functions .................................................................................................................... 27 The Power of JavaScript ............................................................................................... 29 Source Files ..................................................................................................................... 32 What's Next? ................................................................................................................... 32 Prototype and script.aculo.us ............................................................................................ 33 The Todo List ................................................................................................................. 33 Developers Start your IDEs ..................................................................................... 34 From Static to Dynamic ............................................................................................ 38 Final Version ................................................................................................................... 42 A Touch of JSON ..................................................................................................... 43 Data Synchronisation ................................................................................................ 44 Source Files ..................................................................................................................... 46 ii

Copyright Š John Leach 2008. All rights reserved.


What's Next? ................................................................................................................... 46 Review of JavaScript, CSS and XHTML ......................................................................... 47 XHTML Review ............................................................................................................. 47 CSS Review ..................................................................................................................... 47 JavaScript Review ........................................................................................................... 48 Truth ............................................................................................................................ 48 Types ............................................................................................................................ 48 Regular Expressions .................................................................................................. 49 Anonymous Functions .............................................................................................. 49 Unit Testing ................................................................................................................ 51 Your First Unit Test ................................................................................................... 51 Source Files ..................................................................................................................... 54 What's Next? ................................................................................................................... 54 Ruby Full Immersion .......................................................................................................... 55 Ruby Basics ..................................................................................................................... 55 A Ruby Program Dissected ........................................................................................... 57 Source Files ..................................................................................................................... 64 What's Next? ................................................................................................................... 64 Ruby and Ajax ..................................................................................................................... 65 The Word Search Web Application ............................................................................. 65 A Little Architecture .................................................................................................. 65 The Search Classes .................................................................................................... 66 The Server Application ............................................................................................. 68 The JavaScript File ..................................................................................................... 71 The CSS and HTML Files ........................................................................................ 71 Running the Application. .............................................................................................. 72 Functional Degradation ............................................................................................ 74 Source Files ..................................................................................................................... 75 What's Next? ................................................................................................................... 75 Review of Ruby, Overview of Ruby on Rails ................................................................. 76 Ruby Review .................................................................................................................... 76 Iterators and Code Blocks - One More Time ........................................................ 76 Popular Authors and Iterators ................................................................................. 78 Modules - One Solution to Two Problems ............................................................ 79 Unit Testing ................................................................................................................ 82 Overview of Ruby on Rails .......................................................................................... 83 What's in the Box? ..................................................................................................... 84 Models, Views and Controllers ................................................................................ 85 Source Files ..................................................................................................................... 86 What's Next? ................................................................................................................... 86 Ruby on Rails, Part 1 .......................................................................................................... 87 The WebAlbum Application ......................................................................................... 87 Reality Check .............................................................................................................. 87 Using Rails Generators ............................................................................................. 88 Our First User ............................................................................................................ 93 Š Syger 2007. All rights reserved.

iii


Pause for Thought .......................................................................................................... 99 The Master Layout Template ................................................................................. 100 Controllers and Helpers .......................................................................................... 101 First Release .................................................................................................................. 103 Source Files ................................................................................................................... 105 What's Next? ................................................................................................................. 105 Addendum ..................................................................................................................... 105 Ruby on Rails, Part 2 ........................................................................................................ 106 Creating the Albums and Pictures ............................................................................. 106 The Album Controller ............................................................................................ 108 The Album Views .................................................................................................... 110 The Album Model ................................................................................................... 110 The First (and Second) Album .............................................................................. 111 Let's Add Some Pictures ............................................................................................. 111 Uploading Image Files and RMagick .................................................................... 112 The Picture Controller ............................................................................................ 115 Picture Controller and RMagick ............................................................................ 116 The Picture Views ................................................................................................... 118 Finally, Some Configuration ................................................................................... 121 Adding Some Spice with Lightbox ............................................................................ 121 The Results .................................................................................................................... 122 Source Files ................................................................................................................... 124 What's Next? ................................................................................................................. 124 Addendum ..................................................................................................................... 125 © John Leach 2008. Content licensed according to the Artistic License 2.0.

4

Copyright © John Leach 2008. All rights reserved.


LUG Villafranca Programming Course January, 2008 This document gives a brief outline of the programming course organised together with the Linux User Group of Villafranca, Verona, Italy. The course introduces programming in JavaScript, Ruby, and fundamentals of Ajax over a ten week period. In twenty intensive hours, students are introduced to the tools and methods used in modern programming practices, and the author's twenty years of experience.

Source BigFoto.com

This document is being published on the Internet in parallel with the course. Once the course has finished, it will remain as a tutorial for other fledgling programmers.

Course Outline The course has the following objectives: •

• •

Introduce general programming concepts; literals, types, variables, expressions, statements, functions and objects.

values, constants,

Introduce programming in the JavaScript language; development environment, overview of (X)HTML and CSS, simple JavaScript exercises. Advanced programming in the JavaScript language; using the using the script.aculo.us library.

Prototype

library,

Introduce programming in the Ruby language; development environment, simple Ruby exercises. Advanced programming in the Ruby language; Unit testing in Ruby. Using JavaScript for Ajax; using the JavaScript, Ajax mocking.

XMLHttpRequest

object, Unit testing in

Lessons Held The first lesson, on page 9, entitled 'Welcome' took place on Monday, 14th January 2008. The second lesson, on page 11, entitled 'Simple JavaScript Concepts' took place on Monday, 21st January 2008. The third lesson, on page 20, entitled 'Advanced JavaScript Concepts' took place on Monday, 28th January 2008. The fourth lesson, on page 33, entitled 'Prototype and script.aculo.us' took place on Monday, 4th February 2008. The fifth lesson, on page 47, entitled 'Review of JavaScript, CSS and XHTML' took place on Monday, 11th February 2008. Copyright © John Leach 2008. All rights reserved.

5


The sixth lesson, on page 55, entitled 'Ruby Full Immersion' took place on Monday, 18th February 2008. The seventh lesson, on page 65, entitled 'Ruby and Ajax' took place on Monday, 25th February 2008. The eighth lesson, on page 76, entitled 'Review of Ruby, Overview of Ruby on Rails' took place on Monday, 3rd March 2008. The eighth lesson, on page 87, entitled 'Ruby on Rails, Part 1' took place on Monday, 10th March 2008. The eighth lesson, on page 106, entitled 'Ruby on Rails, Part 2' took place on Monday, 17th March 2008. Many thanks to all the participants.

Good Tools = Good Start Programming is a difficult task, but fortunately there are many tools available to ease the burden. Whilst it is perfectly possible to write programs using just a text editor and a compiler / interpreter, we're now well into the 21st century, an overwhelming number of tools are available, and there is no reason to make our lives any harder than they already are. Here then, is the minimal tool list for programming in a specific language: •

• •

An Integrated Development Environment for the language. Try to find the best that you can, and get to know the fundamentals of the program. An IDE, apart from editing text and running your programs, should also provide project management, information about the libraries being used, debugging, compilation, and program packaging. Good language books and tutorials. Learn the fundamentals correctly. Learn the relative strengths and weaknesses of the language. Well written source code examples. Learn from the masters. Unit testing frameworks. Always test your code, feeling, and act as a partial guarantee for the code

it will give you a you produce.

warm fuzzy

Learn to be able to at least read in English. Sometimes mechanical translators such as Babelfish can help.

General Language Agnostic Tools There is one tool that I use as a professional programmer which I cannot live without – Subversion, a modern version control system. Think of it as being a database which can store (historically) each unit of work that you do on your computer. That may sound a little Orwellian, but in fact you are the one who decides what does and does not get stored in the database. Of course you should backup the database regularly. For the course, the use of Subversion is optional, however I will probably make 6

Copyright © John Leach 2008. All rights reserved.


loving references to it on a regular basis. If you want to learn to use it yourself, download Subversion for your operating system, get the book (also available in Italian), and choose a client GUI such as TortoiseSVN for windows, or RapidSVN for Linux and Mac.

JavaScript Tools This course looks at the fundamentals of JavaScript as used within a browser. In order to do this, you will need to install the programs described below on your computer. We'll be using the Firefox web browser, together with the Firebug add-on, which can be directly installed within the browser. However, the IDE we are going to use, Aptana Studio, does this for you so that it can directly connect to Firebug. If you don't want to use Aptana, you can install Firebug from this page. The IDE we'll be using is Aptana Studio Community Edition. Follow the download instructions for your operating system – in all cases, you'll only need the Zip files. On-line documentation is also available, which is also included inside the IDE. The program requires the Java Runtime Environment to be installed for your operating system. The Java Runtime Environment page provides verify buttons to check if you already have Java installed. Extract the contents of the Zip file in the folder where you want the IDE to be stored – I have a windows platform, so I chose C:\DevTools\Aptana. Then launch the application to complete the installation. Aptana has an installation forum, if you have problems installing the software. Once you've got Aptana up and running, open the Plugins Manager view. From the menu bar select Window then Show view then Other..., click on the Aptana Views folder, select Plugins Manager, and click on OK. If you want to version control your projects, you'll want to install the Subclipse plugin.

Ruby Tools This course looks at the fundamentals of Ruby. In order to do this, you will need to install the programs described below on your computer. You will need to download and install the Ruby Interpreter. We'll continue using Aptana Studio for Ruby development on this course. Later on in the course, you'll also need Ruby on Rails, SQLite, and Rmagick. So you'll need to install Ruby on Rails – we're using version 2.0.2. Installing RMagick is explained in this FAQ, we're using version 2.0.0. Installing SQLite is explained in this How-to, we're using version SQLite3 1.2.1. Windows users will need both the .exe and .dll versions of SQLite. The Ruby on Rails Wiki page also gives helpful information for SQLite. Copyright © John Leach 2008. All rights reserved.

7


Ajax Tools As such, the tools outlined above will give you enough to get started developing Ajax applications, however, you will also need to download the Prototype library, and the script.aculo.us library, which we'll be using during this course.

Recommended Reading A re-introduction to JavaScript, Learn to Program,

by Chris Pine.

The Pragmatic Programmer,

by Andrew Hunt and David Thomas.

What (Programming) Languages?

8

by Simon Willison.

by your host.

Copyright Š John Leach 2008. All rights reserved.


Welcome to the Course LUG Programming Course, 14th January 2008 In this first lesson, I spent some time asking the students about their programming backgrounds. There are three web designers, four programmers, three web masters, and an enthusiastic beginner. Their computers ranged from Windows Vista, Windows XP, to Mac OS X, Debian, and Ubuntu. Fortunately the software I have chosen should work on all these platforms. Most of the lesson was spent downloading and installing software. I only managed to ensure that everyone had Firefox and Firebug installed and working. Next week we'll see if Aptana runs OK – some people have 512MB machines, but I hope that's not a problem. Once the downloading, and network connections were worked out, I spent a little time discussing the history of the web from the birth of HTML developed by Sir Tim Berners-Lee in 1989, through Mosaic, Netscape Navigator, to Firefox, Safari, Opera and Internet Explorer. Then I gave an overview of the topics we'll be looking at, together with some simplifications, which are described below.

Simplifications In order to concentrate on JavaScript and Ruby, I am placing some restrictions on our use of HTML and CSS. I'll only be explaining the rudiments of XHTML version 1.0 Transitional DTD. For CSS, we'll stick to basic elements of the CSS 2 standard. The version of JavaScript that we'll be programming to is JavaScript 1.5 which conforms to ECMA-262 Edition 3. All three types of content (XHTML, CSS, and JavaScript) will be kept in separate files, to avoid parsing problems. The reason that I don't want to see JavaScript in a <script></script> tag is partially explained by Douglas Crockford. Apart from the decrement operator, such as index-- within SGML/HTML comments, or decrement operator followed by the greater than symbol index-->0 in HTML/XML comments, there are also problems with HTML in string literals, and life is too short to be worrying about these things.

Observations Almost everyone seemed to know about Firebug for Firefox, which is good news. It is also available for Mac OS X and Linux, unlike two other favourites, Web Developer and HTML Validator, which both seem to be Windows only. Whoops, perhaps not so... Actually both Web Developer and HTML Validator say they are available for Mac OS X and Linux, so perhaps it is the Firefox add-ons site that is Copyright Š John Leach 2008. All rights reserved.

9


wrong (no “Install Now” button on Mac OS X or Linux). I'll check this out next week.

What's Next? Next we'll start JavaScript programming for real, using the Firebug console, and then moving on to a simple HTML/CSS/JavaScript file trio, the following week.

10

Copyright © John Leach 2008. All rights reserved.


Simple JavaScript Concepts LUG Programming Course, 21st January 2008 In this lesson we started to write some simple JavaScript. This was a little tedious for those with previous programming experience, but it served to highlight several important points of the JavaScript programming language. Just as importantly, this lesson helped familiarise the students with the Firebug addon. I was a little too slow starting the lesson, wasting about 30 minutes, so I overran by about 20 minutes, and didn't manage to finish all the material. Fortunately, this first real programming lesson gave me some feedback, so that I can better pace the following lessons. I'm glad to say that I had at least prepared enough material, finishing 30 minutes early might have been embarrassing.

Programming in 60 Seconds What is a computer program? The Columbia Electronic Encyclopedia offers this definition: “A series of instructions that a computer can interpret and execute”. That's a pretty widely scoped statement, so let's think of an example. One task could be to count the number of vowels in the definition. There are 23 of them. The instructions required would include the data; a counter, the definition text, and a list of vowels. They would also include operations that manipulate the data; extract each character of the definition, check if it is a vowel, and increment the counter accordingly. What would that look like in JavaScript? var definition = "A series of instructions that a computer can interpret and execute"; var count = definition.match(/[aeiou]/ig).length;

Alright, I know that this is a bit cryptic, so I'll walk you through it. The first line is fairly simple; we create a named data element (definition) with the definition text. The text is a string, so I have to use quotes, otherwise the JavaScript interpreter would think it was something else. The second line is cryptic. There are three data elements, of which one is named count. The second data element is a regular expression /[aeiou]/ig, and the third data element is the result of calling the definition.match() method, which is an array, for the curious. The array has a property length, which gives us our vowel count. But, there's a But Well, actually the above code doesn't always work. There is one specific input which causes an error. I'll show you how to find (and then correct) the error next week. OK, so you're learning to drive, and I just took you on a rally, in a car with a loose wheel! Don't panic, we'll start with the basics next. Copyright © John Leach 2008. All rights reserved.

11


Simple Expressions Open Firefox, and type about:blank in the location. Next, open Firebug in a separate window (Ctrl F12) â&#x20AC;&#x201C; Firebug may ask you if you want to debug this special (internal) document, so say Yes. Click on the Console tab, then choose Options and select Larger Command Line. Now let's try some literals. On the Command Line type: 3

Then press the Run button, or press Ctrl

Enter.

The result should be:

3

That's an example of a numeric literal. Not too exciting, so try this: Math.PI

Then press should be:

Run.

(I won't keep telling you to press

Run

from now on). The result

3.141592653589793

That's an example of a numeric variable. Let's keep experimenting: typeof 3

The result is: "number"

Ok, so our numeric literal 3 is a number type. (And so is Math.PI). So let's try a simple maths expression:

Math.PI,

try typing

typeof

3 + 4

And the result is, surprisingly enough: 7

Something a little more difficult? 3 + 4 * 7

Which gives the result of 49. Whoops, no it doesn't... 31

So JavaScript can't do maths? Not quite. You probably read that last expression as 3 plus 4 equals 7 by 7 equals 49. But JavaScript, as for any other programming language, uses precedence rules for all operators, including arithmetic operators. We can change the precedence by using parentheses (because parentheses have a higher precedence than + or *): (3 + 4) * 7

Which now gives: 49

Better. But watch out, because JavaScript uses floating point (double-precision 64-bit format IEEE 754 values), which is inaccurate some of the time: 3.1 + 0.2

unfortunately gives: 12

Copyright Š John Leach 2008. All rights reserved.


3.3000000000000003

Something to be aware of (and not just in JavaScript). Normally, you'd define a numeric literal as a base 10 number, but you can use Octal (0666 gives 438 decimal) or Hexadecimal (0x16 gives 22 decimal). Javascript also defines three other numeric values Infinity, -Infinity, and NaN. The first two should be reasonably obvious, 1/0 and 1/-0 will give these values. NaN stands for Not a Number, 0/0 will give this value. Unlike any other numeric value NaN cannot be compared against itself NaN == NaN is guaranteed to be false, so use isNaN(number) instead. Apart from numerics, JavaScript can also handle strings. For example: "hello"

Gives: "hello"

Nothing radical here. You can use single or double quotes as the string delimiters. If you need to add special characters to the string (perhaps embedding a quote), use the backslash character to escape the special character: "\"hello\", he said"

gives: ""hello", he said"

Something more complex: "Backslash is \\ and also \u005c"

gives: "Backslash is \ and also \"

The Unicode code point for backslash is U+005C hexadecimal. Syntax Errors By now, you're getting closer to actual programming code (albeit in very small amounts), so it won't be surprising if you start getting a few syntax errors. A syntax error is a mistake made by the programmer, which the interpreter/compiler cannot correct â&#x20AC;&#x201C; such as forgetting to terminate a string: "Unterminated string\"

gives: SyntaxError: unterminated string literal

A little patience, and you'll be able to figure out where you went wrong. JavaScript uses the + binary operator to concatenate strings: "hello " + "world"

gives: "hello world"

Note, however, that 'adding' strings and numbers together always results in a string: Copyright Š John Leach 2008. All rights reserved.

13


3 + "4"

gives: "34"

and not 7, as would happen in Perl, for example. So non string types get coerced to string types before concatenation with strings. The type of a string can be found by typing: typeof "hello"

which gives: "string"

Finally, let's look at boolean literals. Type: true

which gives: true

Now let's check the (other) boolean literal type: typeof false

gives: "boolean"

Simple Variables Looking to the next stage, we'll create some variables. To create a variable, we start with the var keyword, followed by a name, the assignment operator (=), a value, then a semicolon, which is the statement terminator: var myFirstVar = 1;

which gives: Exactly. It gives nothing back at all. Expressions evaluate to a value, statements don't. However, all is not lost, click on the Script tab, then click on New watch expression... and type this. You should see the myFirstVar variable and its value of 1. Let's see if this is so. Click on the Console tab and type myFirstVar which should result in: 1

Variable Names In JavaScript variable names (and function names) must start with a letter or underscore (_) and can then be followed by letters or numbers. All names are case sensitive, so myvar is a different variable to myVar. Traditionally, JavaScript uses lowerCamelCasing for names. You can't use reserved a lot of them.

words

as a name, and unfortunately, JavaScript has

Now let's create another variable: 14

Copyright Š John Leach 2008. All rights reserved.


var mySecondVar;

Typing mySecondVar gives: Still nothing. In this case, we have not given a value to the variable, so it can't give one back. Just to make the point, type: typeof myFirstVar

which will give: "number"

and then type: typeof mySecondVar

which gives: "undefined"

When a variable is declared, but not initialised, JavaScript gives it the special value of undefined. We can test for this condition, using a comparison operator: typeof mySecondVar == undefined

which gives: true

Note the comparison operator has two equals signs (==) to distinguish it from an assignment operator (=). There is one other special value which we can give to a variable; the null value. This is used when we wish to specifically initialise a variable to nothing. Not a number, or string, or anything else, just nothing. That might not sound very useful, but it is; think about a function that looks up a name, if it finds the name it will return a record of some sort, but what does it return if it can't find the name? Exactly, it returns null.

Simple Functions Functions allow us to group a set of statements under a single name. More than that, we can supply parameters to a function, and get a result back. Remember our vowel counting exercise? If we wanted to count the vowels on any string we could make a function for it. All we'd need to do then would be to pass the string to be counted as a parameter, and let the function give us back the count as the result. Let's try that, type: function countVowels(str) { return str.match(/[aeiou]/ig).length; }

Italian Curly Brackets or Parentesi Graffe If you have an Italian keyboard, you probably won't find the curly brackets, '{' and '}' anywhere. On an English/American keyboard they are the first and second keys to the right of the 'P' key, but with the Shift key pressed. If you don't press the Shift key you get the square brackets, '[' Copyright Š John Leach 2008. All rights reserved.

15


and ']' respectively. I'll call these keys for now.

bracket left

and

bracket right

You can access the square brackets on an Italian keyboard by using Alt Gr and bracket left or bracket right. You can also access the curly brackets with Alt Gr plus Shift and bracket left or bracket right. Next, let's try out our function by typing: var definition = "A series of instructions that a computer can interpret and execute"; countVowels(definition);

which gives 23

Good to know. So let's take a look in a little more detail. To create a function, we start with the function keyword, followed by a name, the open bracket, a comma separated list of zero or more parameters, the close bracket, then the open curly bracket, zero or more statements, and finally the close curly bracket: function name([param] [, param] [..., param]) { statements }

If we want the function to return control to the caller, then we use the keyword, possibly followed by a value.

return

One last point for now; the parameter name does not have to match the name of the variable passed to it. Inside the function, we use the parameter name to reference the parameter, outside the function we use the actual variable name. Think of it as a sort of alias. In our example above we pass the variable definition, but inside the function it is seen as the parameter str.

Conditional Expressions A conditional expression allows us to make tests. This is a fundamental building block of any programming language. It allows us to ask questions, such as “is the sum greater than 10?”. Conditional expressions evaluate to a boolean value of true or false. Let's try a few examples, using the built-in Boolean() method. Type: Boolean(3 > 2)

which gives true

Of course you can use variables instead of literals. Try Boolean(myFirstVar > 2)

which gives false

because myFirstVar is (still) equal to 1. Experiment with other conditional expressions for yourself. You can use >, <, >=, <=, == and != for the binary conditional operator. The last two operators represent “equals”, and “not equals”, respectively. Multiple conditional expressions can be made using the || and && conditional operators. The first is the “or” operator, the 16

Copyright © John Leach 2008. All rights reserved.


second is the “and” operator. Now try this: Boolean(4 == '4')

which gives true

Again, when comparing a number to a string, JavaScript coerces the number to a string. There are two other conditional operators which avoid coercion; === and !==, “strict equals”, and “strict not equals”, respectively. Try this: Boolean(4 === '4')

which gives false

Quick! What values are false in JavaScript? The numbers 0 and NaN, the boolean false, the values null and undefined, and the empty string "". Everything else is true.

Simple Conditional Statements The simplest test we can perform in JavaScript is the if...else... statement. To see what happens, we'll create a simple function, with a basic if...else... statement: function ifTest(expr) { if (expr) { return 'passed'; } else { return 'failed'; } }

Now let's try it out. Type: ifTest(true)

and you should see: "passed"

Type: ifTest(false)

and you'll get: "failed"

So we have created a function similar to the built-in Boolean() function. Try passing some of the previous conditional expressions to our new function: ifTest(3 > 2)

which gives "passed"

The if...else... statement can be nested. Let's say that we want a function which gives grades according to a percentage scale: Grade

Percentage

Copyright © John Leach 2008. All rights reserved.

17


A

90% or better

B

80% or better

C

70% or better

D

55% or better

E

40% or better

F

Less than 40%

This can be done with multiple statements, thus: function calculateGrade(percent) { if (percent >= 90) { return 'A'; } else if (percent >= 80) { return 'B'; } else if (percent >= 70) { return 'C'; } else if (percent >= 55) { return 'D'; } else if(percent >= 40) { return 'E'; } else { return 'F'; } }

This works because when the first conditional expression resolves to true, then that expression's code block (the statements inside the curly braces) will be executed, and all the remaining else if or else statements will be skipped. Because the simplest form of if...else... statement, such as the one we used in our ifTest() method, is so commonplace, there is a shorthand ternary operator which can sometimes be used instead: function ifTest(expr) { return (expr) ? 'passed' : 'failed'; }

Finally, to conclude today's lesson, we'll look at the switch statement. This type of statement can be useful where you have a large set of known values which must be matched. In this case a switch statement can be more concise. In this somewhat convoluted example we'll check for certain 'magic' numbers: function magicNumber(number) { var result = null; switch (number) { case 3.14: case 3.142: case 3.1416: result = 'Approximation of Pi'; break; case 0: result = 'Freezing point of water (Centigrade)'; break; case 32: result = 'Freezing point of water (Fahrenheit)'; break;

18

Copyright Š John Leach 2008. All rights reserved.


case 100: result = 'Boiling point of water (Centigrade)'; break; case 212: result = 'Boiling point of water (Fahrenheit)'; break; default: result = number + ' is not magic to me'; break; } return result; }

Try typing: magicNumber(3.14)

which should give "Approximation of Pi"

But maginNumber(3.1)

gives "3.1 is not magic to me"

Wow! Buy one new keyword (switch) and get three extra (case, default and break). Within the switch code block (the code surrounded by '{' and '}' after the switch expression), we set a label for each case we wish to test. After the label value comes a colon (:), and then the statements for the given case. The statements are terminated by the break keyword and a statement terminating semicolon (;). There is also one special case label called specifically defined by the case labels.

default,

which handles all cases not

You can use switch statements on strings as well as numbers. Care Required with

break;

Take great care with those case label: ... break; sequences. If you forget a break; the code will continue executing the statements of the following case label. This may be desired in some cases, but it's usually a sign of an error.

What's Next? We'll start using a more permanent development environment, using Aptana Studio.

Copyright Š John Leach 2008. All rights reserved.

19


Advanced JavaScript Concepts LUG Programming Course, 28th January 2008 Writing code snippets in Firebug is interesting, but not permanent. As soon as you close the browser, or the tab you were using, all that experimenting is gone forever. The time has come to create a permanent development project. Overran the lesson by half an hour again, and didn't get as far as creating objects, I stopped short of “The Power of JavaScript”. I'm also a little worried that I'm leaving some students behind. To rectify, I'll add a refresher lesson in a couple of weeks.

Your First Aptana Studio Project Start Aptana Studio. Yes, I know it takes a while, so I'll wait... Select File from the menu bar. Then select New and Project..., which opens the New Project Wizard. Under the folder General, select Project, then click on Next. This is going to be the place where we will be doing some experimentation, so we'll call the project Experiments. Uncheck the Use default location check box, and select a folder, such as C:\LUGPC\Experiments on Windows. Finally, click on Finish. You should now see Experiments in the Project view. If you don't have a Project view, you can open it by selecting Window from the menu bar, then Show view, then click on Project. Next we'll create some 'stub' files, but first we'll decide on our folder layout: public/ scripts/ styles/

We'll create a principal folder called public. This is for our 'static' files, into which will go our HTML files. We'll also create two sub folders, one for our JavaScript code, and one for our CSS files. Within the Project view, right click and select hierarchy, and add public.

New,

then

Folder

to create the folder

Next, we'll create a JavaScript file under public/scripts/. Select this folder, then right click, select New, then JavaScript File, and give it the name experimental.js. Now we'll do the same for our CSS file, this time under public/styles/. Select this folder, then right click, select New, then CSS File, and give it the name experimental.css. Lastly, we'll create an XHTML file, under public/. Select this folder, then right click, select New, then HTML File, and give it the name experimental.html. You've probably noticed that Aptana Studio opens the newly created files, and that they already contain some text. We can leave the JavaScript and CSS files alone for the moment, but we need to modify the HTML file. So that we know where we all are, we'll also add line numbering to the editor views; select Window from the menu 20

Copyright © John Leach 2008. All rights reserved.


bar, then Preferences..., then double click on then double click on Editors, then click on Text numbers check box. Finally, click on OK.

General, Editors,

to open the sub-options, and check the Show line

Modifying the XHTML Document

Aptana Studio creates an HTML 4.01 document by default. We actually want an XHTML 1.0 Transitional document. Now, we can make all our changes just using the editor, but since we're working with an Integrated Development Environment, I'll show you how to make the best use of it, and reduce the amount of typing required. Aptana Studio has a useful view called Snippets. If you don't already have this view open, select Windows from the menu bar, then Show view, then click on Snippets. Now click on the start of line 1 of experimental.html, press Shift and the down arrow together, which will highlight the line, then press Enter, which will substitute the first line with a new blank first line, finally, move the cursor up to the start of that line. From the Snippets window, double click on Insert DOCTYPE XHTML 1.0 Transitional. The <DOCTYPE ...> text will be added to our HTML file. To finish our modification, we need to add the XML namespace attribute to our html tag, on line 2. Move the cursor to the '>' character and press the spacebar. Aptana Studio will display a list of attributes in a popup window. Type x, then double click on xmlns. Next, type either a single or double quote, Aptana Studio will create two characters (which delimit the attribute value), and offer a value to insert; http://www.w3.org/1999/xhtml. Double click on the suggestion, and it will be inserted inside the quotes. On line 4, there is a meta tag, which also needs changing. Move the mouse over the content attribute, and a small popup will appear, explaining the attribute. Delete the attribute value (including quotes) after the '=' character, then type a single or double quote, and double click on text/html; charset=UTF-8. Change the title tag contents, on line 5, to LUG something similar.

Programming Course, Lesson 3,

or

Now we have an XHTML document, which uses UTF-8 encoding, the default character encoding for XML documents. Press Ctrl+S to save the file. Adding Tags to the XHTML Document

Next, we want to add tags which link to our (as yet empty) JavaScript and CSS file. On line 5 add a blank line, then in Snippets, double click Insert CSS <link>. Remove the contents of the href attribute value, taking care to keep the double quotes, then type styles/experimental.css. Now we can quickly check that our link is correct, open the experimental.css file, and on line 2 add background: #f00;, press Ctrl+S to save the change, go back to the experimental.html file, and click on the Firefox Preview tab at the bottom of the window. You should see a red screen, which was white before our changes were made. Finally, create a new blank line on line 6, so we can add our JavaScript file. In Copyright Š John Leach 2008. All rights reserved.

21


Snippets, double click Insert JS <script> external, and type scripts/experimental.js for the src value. Again, we'll make a quick check that the link is correct, open experimental.js, and on line 4 add alert('Connected');, press Ctrl+S to save the change, go back to experimental.html, and click on the Firefox Preview tab. You should see a popup dialog with the title JavaScript Application, and the text Connected. Click on OK.

Now remove the modifications made in experimental.css and experimental.js, because red backgrounds, and popup alerts are definitely out of fashion. Why Check the Links? Because if they're wrong, the browser simply won't tell you. Try it and see; change scripts/experimental.js to scripts/missing.js. Now check out the Firefox Preview. No error messages or warnings to be seen. You won't see any in Firebug either. Same with Aptana Studio, even in debug mode. So before you spend hours trying to understand why your styles or code aren't working, make sure they are linked and loaded correctly first. Oh,

by

the

way,

change scripts/missing.js scripts/experimental.js. We don't want any missing links!

back

to

Our XHTML file should now look something like: 01 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 02 <html xmlns='http://www.w3.org/1999/xhtml'> 03 <head> 04 <meta http-equiv="Content-Type" content='text/html; charset=UTF-8' /> 05 <link rel="stylesheet" href="styles/experimental.css" type="text/css" media="screen" charset="utf-8" /> 06 <script src="scripts/experimental.js" type="text/javascript" charset="utf-8"></ script> 07 <title>LUG Programming Course, Lesson 3</title> 08 </head> 09 <body> 10 </body> 11 </html>

Running the Debugger Aptana Studio doesn't just give a helping hand writing our source code, it also provides a great debugging environment with the help of Firefox and Firebug. To debug our code, we need to create a debugging configuration. Select Run from the menu bar, then click on Debug.... This opens the Debug Configuration Wizard. Click on the Web browser option, then on the new configuration icon (top left). Change the name to Experimental, and in Start Action, check the Specific page option, click on Browse..., and select experimental.html. Finally, click on Apply, then Close. Now we need some code to debug. Last week I mentioned that the following code doesn't always work for all strings: 22

Copyright Š John Leach 2008. All rights reserved.


function countVowels(str) { return str.match(/[aeiou]/ig).length; }

So now we can find out when it doesn't work, and why. First we'll need the function, then we'll call it with two examples, and send the result to Firebug's console using console.log(): 4 5 6 7 8 9

function countVowels(str) { return str.match(/[aeiou]/ig).length; } console.log('countVowels("Hello"): ' + countVowels("Hello")); console.log(" countVowels('Krk'): " + countVowels('Krk'));

Type the above into experimental.js, which we'll test in the debugger. Select Run from the menu bar, then Debug..., and then select Experimental and click on the Debug button. You may be asked if you want to use the Debug prospective for your debugging session. As this is the most useful prospective, answer Yes. Aptana Studio opens the Firefox browser in order to communicate with the Firebug add-on. Since our script causes an error, and, by default, debugging halts on errors, you should see that the debug prospective halts on line 5 of experimental.js in Firebug. The Aptana Studio console also produces the error information: TypeError: str.match(/[aeiou]/gi) has no properties at countVowels(String) (Experiments/htdocs/scripts/experimental.js:5) at [Top-level script]() (Experiments/htdocs/scripts/experimental.js:9)

It also produces the Firebug console output of: countVowels("Hello"): 2

Some Detective Work Required

The code failed, giving a TypeError on line 5, while executing the code on line 9. This was the second call to countVowels(), but the first call to countVowels() worked correctly, and gave the correct answer of 2. The difference is that the second call passed a string which has no vowels â&#x20AC;&#x201C; Krk. So, Croatian islands are to blame! No, it's more likely that match() is giving unexpected results. Unfortunately, the statement on line 5 is too compact to give us further information. You can add a breakpoint at line 5, by double clicking at the extreme left of the line (in the grey side bar), which causes a small blue dot to appear, but even stepping into the code doesn't provide any more information. However, we can rewrite it in a simpler, if more verbose form: 4 function countVowels(str) { 5 var regExp = /[aeiou]/ig; 6 var matchResult = str.match(regExp); 7 return matchResult.length; 8 }

Stop the current debugging session by selecting Run from the menu bar, then click on Terminate. Change the countVowels() function to the above code, and start the debugger again by selecting Run from the menu bar, then click on Debug Last Launched, or more simply, press F11. This time the debugger stops at line 7. But now we have some more information to Copyright Š John Leach 2008. All rights reserved.

23


look at. The TypeError now tells us that matchResult has no properties, and, looking in the Variables window, we can see that it is actually null. Moving the mouse over matchResult on line 6 of the code also shows this value. So, although the official documentation doesn't say as much, the match() function actually returns an array of matching strings, or null if no matches are found, rather than an empty array, as I had supposed. Nice to know, sigh. Since there are no vowels in Krk, it returns null. As null is not an array, it doesn't have a length property (in fact null has no properties at all). Now we need to find a solution which fixes this problem. We've debugged the code, now we're bug fixing. We can use the ternary operator to specifically test for null, in which case we'll return 0. Our final (and hopefully bug free) function now becomes: 4 function countVowels(str) { 5 var result = str.match(/[aeiou]/ig); 6 return (result) ? result.length : 0; 7 }

Stop the current debugging session by selecting Run from the menu bar, then click on Terminate. Change the countVowels() function to the above code, and start the debugger again by selecting Run from the menu bar, then click on Debug Last Launched, or more simply, press F11. Finally, our program runs to completion, with the expected and correct console log of: countVowels("Hello"): 2 countVowels('Krk'): 0

“O frabjous day! Callooh! Callay!”,

He chortled in his joy.

Other JavaScript Types We've already talked about numbers, strings and boolean types. Now we'll take a look at arrays, associative arrays, regular expressions, and functions. Yes, I did write functions, they're a JavaScript type too. Arrays

An array is a list of values that can be accessed by a zero based index. Let's have a look at an example, using the Firebug console: var array = [ 'Centigrade', 'Fahrenheit', 'Kelvin' ];

The first element of the array has the index value of 0, the second 1, and so on. The length of the array is given by the length property. Assigning a value using an index greater than or equal to the length causes the array to be increased in size, and undefined elements will have the value of undefined: array[5] = 'Tulips'; array

gives: ["Centigrade", "Fahrenheit", "Kelvin", undefined, undefined, "Tulips"]

24

Copyright © John Leach 2008. All rights reserved.


We can access the second element of the array with: array[1]

which gives: "Fahrenheit"

We can find the array length with: array.length

which gives: 6

Specifying invalid index values (< 0, or >=

length),

gives undefined, so:

array[10] === undefined

results in: true

Most importantly, we can use a 'classic' for loop to iterate over our array: for (var i = 0, l = array.length; i < l; i++) { /* do something useful with */ array[i]; }

The 'classic' for loop consists of three sections, separated by semicolons (;), the initialising statement (var i = 0, l = array.length), the conditional expression (i < l), and the final statement (i++). Yes, you can create multiple variables, separated by commas (,), and yes, you can increment a variable by one using ++. The statements inside the for code block are executed until the conditional expression is true. This could happen immediately, in which case the code block is not executed at all, such as when the array length is 0. Arrays also have a few methods, the most important of which are push() and join(). The push(value) method adds an element to the end of the array, which saves you having to type something like array[array.length] = value;. The join(separator) method converts an array into a string, with each element separated by the separator parameter string. The join() method uses ',' as the default, so if you want no separator at all use the empty string (''). Associative Arrays

Associative arrays are collections of name/value pairs. They are sometimes also known as dictionaries, hashes, lookup tables, or maps. JavaScript has no special type for associative arrays, rather it uses the object type. In other words the JavaScript Object is also an associative array. Let's look at an example, still using the Firebug console: var map = { 'Fahrenheit': 32, 'Centigrade': 0, 'Kelvin': 273.15 }; map

which gives: Object Fahrenheit=32 Centigrade=0 Kelvin=273.15

A specific value can be accessed using the corresponding name, thus: map['Kelvin']

Copyright Š John Leach 2008. All rights reserved.

25


gives: 273.15

If the name is also a valid JavaScript identifier, and does not correspond to a keyword, then dot notation can be used: map.Human = 'Freezing cold!'; map.Human

gives: "Freezing cold!"

Accessing a value using a name that does not exist in the associative array returns undefined, thus: map['Polar Bear'] === undefined

results in: true

Associated arrays use a special form for the for loop known as for...in. Associated array iteration looks like the following: for (var key in map) { /* do something useful with */ map[key]; }

Regular Expressions

One of the most repetitive jobs in programming is string manipulation, parsing, and validation. It is such a repetitive operation that most modern languages provide some form of regular expression implementation, which provides a means of analysing strings, using a concise grammar. In the JavaScript world we're looking at, regular expressions are frequently used to check the validity of form input fields, such as email addresses. Unfortunately, the grammar of regular expressions is (or can be) somewhat complex. We used a regular expression to find all single occurrences of vowels in a string: var re = /[aeiou]/ig; // same as var = new RegExp('[aeiou]', 'ig');

The Prototype library, which we'll look at next week, also makes use of regular expressions to strip leading and trailing whitespace from strings (slightly reorganised here for clarity): function strip(str) { return str.replace(/^\s+/, '').replace(/\s+$/, ''); } strip("\n \t \u00a0Goodbye Cruel World\n \t \u00a0")

gives: "Goodbye Cruel World"

The first regular expression (/^\s+/) matches leading whitespace, and the second (/\ s+$/) matches trailing whitespace. Curiously, I have found that the two replace() method calls can be substituted by one call â&#x20AC;&#x201C; with a slightly altered regular expression: function strip(str) { return str.replace(/^\s+|\s+$/g, ''); } strip("\n \t \u00a0Goodbye Cruel World\n \t \u00a0")

26

Copyright Š John Leach 2008. All rights reserved.


also gives: "Goodbye Cruel World"

I have not seen this solution used in the wild (particularly, it is not used in Prototype). Ruby's squeeze() method, which compacts all whitespace sequences to a single space (\u0020), could also be implemented using a regular expression, for example, in the Firebug console: function squeeze(str) { return str.replace(/\s+/g, ' '); } squeeze("Goodbye\n \t \u00a0Cruel\n \t \u00a0World")

gives: "Goodbye Cruel World"

The easiest way to create a regular expression is to use a nearly equivalent existing expression, and make modifications, using example text in a regular expression tester. Functions

Though we've become quite familiar with functions by now, there are still a few interesting points to note. A function is a JavaScript type. It is an Object and has properties. This is vitally important when extending other Objects, as we'll see a little later on. The most important property of a function is its arguments. Actually, to be more correct, arguments are a local variable of the function during its execution. Up to now, we have specifically defined our function parameters by name. Most modern programming languages are able to provide optional parameters, or variable length argument lists. JavaScript achieves this by providing the arguments array. An example will help a lot here. Let's create a function which sums a series of numbers. That's any quantity of numbers, including zero: function sum() { var total = 0; for (var i = 0, l = arguments.length; i < l; i++) { total += arguments[i]; } return total; }

There are two things to note here; firstly we don't specify any parameters at all, and secondly, we iterate the arguments array just as for any other array. Let's try it out in the Firebug console. Type: sum()

gives: 0

Whereas sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

gives Copyright Š John Leach 2008. All rights reserved.

27


55

But what about the opposite situation, where fewer arguments are given than the number of parameters required? Each unspecified argument becomes an undefined parameter. Now, we could test for each parameter being undefined, but there is a quicker way, which takes advantage of two facts; to JavaScript undefined is false, and secondly that conditional logical or operators (||) will short circuit if the left hand side is true. Let's take a look at an example: function quote(obj, open, close) { obj = obj || ''; open = open || '"'; close = close || '"'; return open + obj + close; }

Again, we'll test this little function in the Firebug console. Type: quote()

gives: """"

Whereas quote(123.23)

gives ""123.23""

Finally: quote(123.23, '(', ')')

gives "(123.23)"

Just one small observation to make here. The empty string ('') is also false, so the function will always supply the default open and close quotes, even if you specified: quotes('hello', '', '')

The classic developer's response? “That's not a bug, that's a feature!”. But it's perhaps arguable that this is a feature that needs correcting. I said previously that a console:

function

is a JavaScript type. Type this in the Firebug

typeof quotes === 'function'

which gives: true

Strange as it may seem, a function type also has methods. There is one interesting method called apply(). In our sum() function above, we passed the values as individual parameters. But what if we already have an array of numbers to be summed? We can do this: var numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]; sum.apply(this, numbers)

which gives: 55

28

Copyright © John Leach 2008. All rights reserved.


The Power of JavaScript JavaScript's Object is the real power of the language, together with the fact that everything we've seen up to now, numbers, strings, variables, and functions, are all Objects too. So let's move on to object oriented programming in JavaScript. Up to now, all of our examples have been procedural in nature. But this was just a simplification, because in reality JavaScript is an object oriented programming language. We previously used the keyword this to find our defined variables and functions in Firebug. For all the work we've done up to now, this effectively corresponds to the global object of the JavaScript environment. Every variable we created became a property of the global object, and every function became a method of the global object. Inside the browser, the global object is window. Let's put together a small object oriented example, which we'll store in our experimental.js file: 15 String.prototype.squash = function () { 16 return this.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' '); 17 }; 18

This first short example actually adds a method squash to an existing built in type, String. There are two things to note here. Firstly, we are assigning a function definition to a property name, so we have to use the assignment syntax, name = value;. This is possible because a function in JavaScript is a type, like a string or number. In fact, we are adding a property, called squash to the prototype of String. So just as we have existing methods such as String.replace(), we also now have String.squash(). Secondly, in the function body we refer to the string object itself using the this keyword, accessing the replace() method of that specific string. 19 var Validator = { 20 21 // Returns the string, or null if not numeric 22 numeric: function (str) { 23 str = str.squash(); 24 var result = (/^\d+$/i).exec(str); 25 return (result) ? result[0] : null; 26 }, 27 28 // Returns the string, or null if not a valid email 29 email: function (str) { 30 str = str.squash(); 31 var result = (/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i).exec(str); 32 return (result) ? result[0] : null; 33 } 34 }; 35

In this second example there are also a few things to note. Firstly, we are using the associative array syntax to create our object: var Validator = { /* ... */

};

Secondly, we have altered our function definition syntax slightly, in order to stay in line with the name: value, ... syntax of an associative array: Copyright Š John Leach 2008. All rights reserved.

29


email: function (str) { /* ... / } // instead of function email(str) { / ... */ }

Thirdly, each function is separated by commas, as required by the associative array syntax. Finally the associative array is terminated with a semicolon (;). The two validator methods numeric() and processing method, squash().

email(),

both make calls to our string

Next we'll add code to test our new methods: 36 console.log("Squash '\\n\\t\\u00a0Goodbye \\t\\u00a0 World\\n\\t\\u00a0': '" + "\n\t\u00a0Goodbye \t\u00a0 World\n\t\u00a0".squash() + "'"); 37 console.log(" Numeric ' 000123 ': '" + Validator.numeric(' 000123 ') + "'"); 38 console.log(" Email ' mickey_and_minny@mouse.co.uk ': '" + Validator.email(' mickey_and_minny@mouse.co.uk ') + "'");

Which produce the following results: Squash '\n\t\u00a0Goodbye \t\u00a0

World\n\t\u00a0': 'Goodbye World' Numeric ' 000123 ': '000123' Email ' mickey_and_minny@mouse.co.uk ': 'mickey_and_minny@mouse.co.uk'

Our Validator object in reality is a singleton pattern with create a real multiple instantiation object.

static methods.

Time to

Up to now, we have been relying on Firebug, and its console object to display the results of using our functions. That's fine if we're only going to test Firefox, but what about the other browsers? They may not have a debugger like Firebug, or even a console object. We can create a simple logger object for ourselves, which we'll do in a minute, first though we'll change all occurrences of console.log() to logger.log() in our experimental.js file â&#x20AC;&#x201C; there are five of them. Next we'll add a <script> tag to our experimental.html page on line 6, and a <div> container tag on line 11, thus: 05 <link rel="stylesheet" href="styles/experimental.css" type="text/css" media="screen" charset="utf-8" /> 06 <script src="scripts/debugging.js" type="text/javascript" charset="utf-8"></script> 07 <script src="scripts/experimental.js" type="text/javascript" charset="utf-8"></ script> 08 <title>LUG Programming Course, Lesson 3</title> 09 </head> 10 <body> 11 <div id="log-output"></div> 12 </body>

Finally, in the public/scripts/ folder we'll create our debugging.js file, which will contain the following code: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

30

/* * Debugging utilities */ function Logger() { this._console = null; if (window.console && window.console.log) { this._console = window.console; } this.messages = []; } Logger.prototype = { log: function (msg) { msg = msg || ''; this.messages.push(msg);

Copyright Š John Leach 2008. All rights reserved.


16 if (this._console) { 17 this._console.log(msg); 18 } 19 }, 20 21 show: function (id) { 22 if (id && (id = document.getElementById(id))) { 23 id.innerHTML = "<pre>" + this.messages.join('\n') + "</pre>"; 24 } 25 else if (!this._console) { 26 alert(this.messages.join('\n')); 27 } 28 } 29 }; 30 31 var logger = new Logger();

Our Logger class is divided into two sections. Lines 4 to 10 contains the constructor method. As its name implies, this is the method called to construct an instance of the class, and line 31 shows an example. Note that, by convention, constructor methods start with an uppercase letter. Also note that we create instance variables using the this keyword, without using the var keyword (line 5). Unfortunately, that can be the cause of serious problems because if you misspell the name (this._cosole) you'll simply create a new property – you won't see a 'missing property' runtime error. You have been warned. The second section, lines 12 to 29 defines the prototype of the class. Here we have two methods, log() which works in a similar fashion to Firebug's console.log(), and show() which displays all the logged messages. Clearly, it would be a shame to stop using Firebug's console object when it's available. Lines 5 to 8 check if it is indeed available, in which case it keeps a local reference. Remember that window is the global object, and that we can check properties which are methods without invoking the method. So line 6 reads: “if window has a console property, and window.console has a log property then...”. It's not foolproof, we might find some browser with a console object and log property which is not a function, but that seems pretty improbable. Our log method, lines 13 to 19, takes a message (defaulting to the empty string if none is provided), adds it to the internal array of messages, and passes it on to the console.log method, if one exists. The show method, lines 21 to 28, displays all the messages logged up to now. It does this in three different ways; firstly (line 22) if you supply an identifier to an HTML tag, it will dynamically add the messages as HTML markup to that tag, secondly if you don't supply an identifier and there is no console.log (line 25), it will show the messages in a JavaScript alert dialog. The third option does nothing (if you don't supply an identifier and console.log exists) because the messages will already have been sent there. To add HTML markup dynamically, we use a method document.getElementById(), and a property element.innerHTML. The method gets the element with the specified identifier, and the property substitutes all the elements' child nodes with the given markup. Copyright © John Leach 2008. All rights reserved.

31


Since I prefer to avoid alert dialogs, we will trigger the call to the logger.show() method by adding it to the onload event in the <body> tag, on line 10 of our experimental.html file: 10 11 12

<body onload="logger.show('log-output');"> <div id="log-output"></div> </body>

The onload attribute tells the browser to evaluate the attribute value, when the document is loaded. In this case it will call our logger.show() method, which will change the contents of the tag with identifier equal to ' log-output', which just happens to be on line 11. Reloading our experimental.html page should now show: countVowels("Hello"): 2 countVowels('Krk'): 0 Squash '\n\t\u00a0Goodbye \t\u00a0 World\n\t\u00a0': 'Goodbye World' Numeric ' 000123 ': '000123' Email ' mickey_and_minny@mouse.co.uk ': 'mickey_and_minny@mouse.co.uk'

Although we could have created our Logger class as a singleton, as we did with validator, there may be situations where you need more than one. Perhaps one per JavaScript file, or one for your code, and one for someone else's library code. That way you can pick and choose the information to display, without getting swamped in an avalanche of messages.

Source Files All the source files for this lesson, including the Aptana project file can be found in the LUGPC3.zip archived file, distributed under the GNU Lesser General Public License.

What's Next? We'll take a look at the Prototype and script.aculo.us libraries, and how to dynamically modify a web page in more elegant ways.

32

Copyright Š John Leach 2008. All rights reserved.


Prototype and script.aculo.us LUG Programming Course, 4th February 2008 The third lesson showed some of the power available in the JavaScript language, but also kept clear of the uneven surfaces that exist in currently available browsers. The plain fact of the matter is that browsers behave differently, and they are not perfect. Unless you want to become a specialised consultant, most web programmers don't have the time, energy, or resources to understand, and find solutions to these problems. This fourth lesson makes use of two popular open source JavaScript libraries (Prototype and script.aculo.us) which take care of smoothing out the browser bumps, and create some very professional results, with very little code required by the developer. This time I managed to recover the part of last week's lesson that I didn't finish, and get though this lesson with about 15 minutes to spare. Unfortunately, I lost about a third of the class along the way. I spent half an hour going over the concepts of the Document Object Model, and anonymous functions, but we'll have to look at these again in the next lesson, which will be a review of the things we have seen up to now.

The Todo List We'll use Prototype and script.aculo.us to produce a todo list. In fact we want two lists; the “to do” list, and the “done” list. Both lists will be provided as JavaScript objects – actually, we'll use JSON (JavaScript Object Notation) for them both. We'll use the libraries to render the lists as HTML markup, and to change the list order, and swap items from one list to the other. We want the user to be able to change the list order, and swap items, by simply dragging the item around or between the lists. Obviously, you will need the script.aculo.us library, and we'll be using version 1.8.1 for this course. This library also includes version 1.6.0.1 of the prototype library. The full prototype library, including unit tests, can be downloaded using Subversion, though this isn't necessary for our lesson, and also requires Ruby to be installed to be able to build the distribution library. The script.aculou.us library also comes with a fairly comprehensive set of unit tests. These serve a dual purpose, apart from testing that the library and its dependencies work, it also demonstrates some of the effects available. We'll be building on the test/functional/sortable4_test.html example in the functional test suite (test/ run_functional_tests.html). Our specification is reasonably lean (just the first paragraph of this section), we've got a reasonable example, but there are still some building blocks that we'll require: •

Create the HTML page

Create the styles

Make use of events to update the lists Copyright © John Leach 2008. All rights reserved.

33


Make the lists draggable and droppable

Define our JSON objects

Convert our JSON objects to HTML

Keep the JSON objects synchronised with the HTML lists

Developers Start your IDEs

We'll create a new project in Aptana Studio, which we'll call will have the familiar folder pattern:

SortableLists,

and it

public/ scripts/ styles/

Take lib/prototype.js, src/builder.js, src/dragdrop.js and src/effects.js from the script.aculo.us download and put them in public/scripts/. Create a CSS file called lists.css in the public/styles/ folder, create a JavaScript file called lists.js in the public/scripts/ folder, and finally create an HTML file called lists.html in the public/ folder. Next, we'll modify the lists.html file so that it has some, er, lists: 01 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 02 <html xmlns='http://www.w3.org/1999/xhtml'> 03 <head> 04 <meta http-equiv="Content-Type" content='text/html; charset=UTF-8' /> 05 <link rel="stylesheet" href="styles/lists.css" type="text/css" media="screen" charset="utf-8" /> 06 <script src="scripts/prototype.js" type="text/javascript" charset="utf-8"></script> 07 <script src="scripts/builder.js" type="text/javascript" charset="utf-8"></script> 08 <script src="scripts/effects.js" type="text/javascript" charset="utf-8"></script> 09 <script src="scripts/dragdrop.js" type="text/javascript" charset="utf-8"></script> 10 <script src="scripts/lists.js" type="text/javascript" charset="utf-8"></script> 11 <title>LUG Programming Course, Lesson 4</title> 12 </head>

The <head> of the document isn't new to us. It just contains links to our CSS and JavaScript files, and a title. The <body>, however, contains our first visible markup. Although there are a large number of elements available to us, we're just going to use five of them. To break our page up into divisions (think panels), we use the <div> element. All the titles use paragraph elements, <p>. The lists are made up of unordered lists <ul>, which contain the list items <li>. Our fifth element <pre>, is for preserving the formatting of output from our (yet to be written) JavaScript code: 13 14 15 16 17 18 19 20 21 22 23

34

<body> <p class='title'>Example of prototype/script.aculo.us usage</p> <div id='todo'> <p>Todo list:</p> <ul id='todoList'> <li id='todo_4'>Using Professional JavaScript Libraries</li> <li id='todo_5'>JavaScript Refresher Lesson</li> <li id='todo_6'>Ruby Basics Lesson</li> <li id='todo_7'>Ruby on Rails Basics Lesson</li> <li id='todo_8'>Ajax Basics Lesson</li> <li id='todo_9'>Ruby Refresher Lesson</li>

Copyright © John Leach 2008. All rights reserved.


24 <li id='todo_10'>Party Time, Questions and Maybe Some Answers</li> 25 </ul> 26 </div> 27 <div id='done'> 28 <p>Done list:</p> 29 <ul id='doneList'> 30 <li id='todo_1'>Welcome and Software Installation Lesson</li> 31 <li id='todo_2'>JavaScript Basics Lesson</li> 32 <li id='todo_3'>Advanced JavaScript Lesson</li> 33 </ul> 34 </div> 35 <pre id="feedback">...</pre> 36 </body> 37 </html>

What? Too much to type? Alright, I know this isn't a typing course, so just pick up the text file, and rename it lists.html. Now click on the browser preview and lets take a look at what our page looks like:

Ok, no design awards for 20th century style, we'll make things a little better in a moment using CSS. The world of XHTML elements is roughly divided into two; block elements and inline elements. We've only used block elements in our XHTML page, that is to say, elements which break up the flow of text. Every paragraph of this tutorial is a block. Inline elements, such as <img> for images, do not change the flow of text. In that last sentence, I used a <span> inline element to change the font for <img> to monospace. Two other things to note is that I have used the class and id attributes on some of the elements. The class attribute allows me to create a specific style for a set of elements. More than one element can have the same class name, and the class attribute can specify more than one class name, separated by whitespace. We'll see more of this in a moment. The id attribute is a powerful helper for JavaScript that has to examine the document content. An id attribute value is much like a JavaScript variable name, and must also be unique within the document. I've created an identifier for each list, todoList and doneList, so that we can access them very efficiently from JavaScript, as we'll see a little later on. Copyright Š John Leach 2008. All rights reserved.

35


You can learn more about XHTML by checking out the World Wide Web Consortium, they have a brief tutorial, and a big, boring but authoritative specification. Moving on, we need to make that web page look a little more attractive. CSS rules syntax is quite similar to JavaScript syntax, you start with a comma separated list of selectors, then the style properties cosily wrapped up in curly braces. The style properties are name/value pairs, which have a colon (:) as the separator, and semicolon (;) as the terminator. Comments can also be added, but use the /* comment */ format only. Again, you can learn more about CSS by checking out the World Wide Web Consortium, they have a brief tutorial, another big, boring but authoritative specification, and a home page with other information. To keep things simple, we'll stick to simple selectors. If we use a name, we are referring to an element name, such as body, div or ul. If the name is prefixed with a dot (.), such as .todo, then we're referring to a class name, which we previously placed in a class attribute in our XHTML. If the name is prefixed with a hash ( #), then we're referring to an identifier, again, which we previously placed in an id attribute in our XHTML. Open lists.css and add the following code, which I'll explain as we go along: 01 body { 02 font: 12pt Verdana, Helvetica, sans-serif; 03 width: 34em; 04 } 05

Units of measure in CSS come in two flavours, relative and absolute. Relative units can be a percentage (such as 125%), em, ex or px. An em is equivalent to the size of the biggest character in the font (usually the letter m), an ex is equivalent to the size of the average character in the font (typically the letter x), and a px is a pixel. Absolute units are in, cm, mm, pt and pc. An in is an inch (2.54 cm), a cm is a centimeter, a mm is a millimeter, a pt is a printers point (1/72 of an inch), and a pc is a printers pica (1/6 of an inch). As a side note, always try to avoid using px (pixel) units for anything except image sizes. It's the easiest to use, requiring little or no thought. But how large is your user's monitor? Today, or in two years time? 06 07 08 09 10 11 12 13 14 15

.title { font-size: 14pt; margin: 0.15em; } #todo, #done { float: left; width: 16.5em; }

The float property is interesting in that it allows us to 'float' blocks on the page, which would otherwise be rendered one below the other. We use it here to get our two lists to appear side by side. 36

Copyright Š John Leach 2008. All rights reserved.


16 17 18 19 20 21 22 23 24 25 26

p { margin: 0.2em; } ul { list-style: none none; font-size: 11pt; margin: 0; padding: 0; }

Unordered lists usually have bullets or other graphics before each item. Using the list-style property, we can change this behaviour. In our case, we get rid of them altogether. 27 li { 28 border: 1px solid; 29 margin: 0.2em; 30 padding: 0.2em; 31 cursor: move; 32 } 33

For each <li> element we set the that it can be dragged. 34 35 36 37 38 39 40 41

cursor

to

move,

so that the user will understand

#todoList li { background-color: #ecacac; } #doneList li { background-color: #acecac; }

The last two style rules have slightly more complicated selectors. Basically, they change the background colour of all <li> elements that are descendants of the two identifiers todoList and doneList. 42 pre { 43 clear: both; 44 font-size: 8pt; 45 }

The clear property can be used to reset any previous float property values. This guarantees that our <pre> element will appear below the two lists. Still too much to type? Just pick up the take another look:

text file,

and rename it

lists.css.

Copyright Š John Leach 2008. All rights reserved.

Now we'll

37


That certainly looks a little better, though I still don't think it'll win any design awards. One cautionary note about syntax errors in CSS files must be made. The browser won't tell you if there are any. It will either ignore the syntactically incorrect rule, or one or more of the syntactically incorrect properties. In total silence. This can be the cause of much grief and waste of time. If you think the page rendering is incorrect, check out what Firebug tells you. If you can't find the style, it probably got erased due to a syntax error. Firebug, fortunately, reports such syntax errors. From Static to Dynamic

Now we'll add the JavaScript code to make the two lists sortable and exchangeable. Because we're using a library, we don't really need to write much code. First we create a helper object to manage the lists, and more importantly, the feedback from changes made to the lists: 01 /* 02 * Sortable Lists 03 */ 04 var SortableLists = { 05 lists: ["todoList", "doneList"], 06 07 updated: function (list) { 08 // does nothing for now... 09 }, 10

Our variable SortableLists has an array containing the identifiers of our lists, and a method to handle an update of the list (list reordered, or an item was added or removed), which does very little for now. 11 12 13 14 15 16 17 18 19

38

createSortables: function (event) { var lists = SortableLists.lists; lists.each(function (id) { Sortable.create(id, { dropOnEmpty: true, containment: lists, constraint: false, onUpdate: SortableLists.updated });

Copyright Š John Leach 2008. All rights reserved.


20 }); 21 } 22 }; 23

Our final helper function is responsible for making our two lists sortable. The method receives an event object (which it doesn't actually make use of), and then, with the help of some Prototype and script.aculo.us methods, adds the sortable magic to the list elements. I'll leave the discussion about events till a little later, meanwhile we've got a few functions and methods here which you'll need a little help with. Prototype arrays have many more methods available, including several known as Enumerable. These provide powerful mechanisms for iterating collections. We can 'walk' through our lists array using the each() method, line 13. It basically hands each item in the array as a parameter to a function, which the programmer supplies. The function itself (lines 13 to 19) is anonymous, because it is only used in one place. We don't need to clutter an object with function names that only get used once. This is a recurring theme in the Prototype library. See the Prototype documentation for more information. Inside our anonymous function, on line 14, we call the script.aculo.us Sortable.create() method, for each list in our lists array. This method uses powerful magic to make some part of the XHTML document sortable. It accepts one required parameter (the identifier of the element to make sortable), and a host of options. Since a programmer may want to change none, one, or half a dozen of these options, the library uses a very smart technique – a map. So, in the map, we can specify just the options we wish to change. All the others will maintain their default values. By the way, documentation of script.aculo.us is available from their wiki. Typing Sortable.create in the search form (yes, it does use auto-completion, which is available from the library itself), yields the relative documentation page. The options we use tell the Sortable.create method that: •

We want to be able to drop on an empty list (line 15); dropOnEmpty:

The user can only drop on our two lists (line 16);

true

containment:

SortableLists.lists • •

There are no (other) constraints (line 17); constraint:

false

When the order changes, or when items are added or removed, call our helper function (line 18); onUpdate: SortableLists.updated

One important point is that Sortable makes use of a simple naming convention within the identifiers in each <li> element in order to move the element around. The only remaining piece in the puzzle is to make sure that our SortableLists.createSortables() method gets called. 24 document.observe("dom:loaded", SortableLists.createSortables);

Still tired of typing? OK, grab the text file then. Remember to rename it to lists.js. We use a Prototype method document.observe() to add a function which will be called when an event happens. In this case the event is dom:loaded, which means that the DOM (Document Object Model) has been fully loaded into memory. Nota bene: Copyright © John Leach 2008. All rights reserved.

39


We pass the function as the parameter, we don't actually call the function document.observe() will do that at the right time. Until our XHTML document is loaded into a browser, it's really just a piece of text. When loaded into the browser, it becomes a hierarchical set of objects, bristling with properties and methods, which we can then manipulate programmatically. That's the DOM. But we've got to be careful not to touch it until it's complete, and that's what the dom:loaded event tells us. Finally, we can check out that everything works. Try it out in Firefox. Grab an item, drag it around, see what happens. I personally am very impressed, because I know how hard this is to do. Well, it's true that we loaded 6,000 lines of code from the two libraries, but we only had to write 26 lines of our own code (at least, up to now). How long would it have taken to write code to create that much dynamic action? One week? One month? But, there are some problems. Move all the done elements to the to do list. Now try moving one back again. Oops, it can't go back. This is because the done list has zero height, so the drop zone is never triggered (see the Firebug information, below). We'll fix this in our final version.

Now try it out in some other browsers. Oops, this is what happens in Opera 9.1:

40

Copyright Š John Leach 2008. All rights reserved.


It happens when you move the top item down. It doesn't happen when you move any other item. The Error Console doesn't give any consolation either. Welcome to the exciting world of the web developer! So, we need to try some experimentation. After a couple of hours (really), I found out that the problem goes away if you add a small margin and padding to the ul element of each list – 0.05em will do. Fine, so we check everything works again, but, look what happens now in Internet Explorer 7:

Hmm. There is a recurring theme here – problems with the first <li>. Empty drop zone, Opera problem, IE problem. We'll cure all these problems in our final version by supplying a non sortable first <li>. Are we done now? Well, let's try something a little more rigorous. Try enlarging the page (press Ctrl++ a few times). Perfect in Firefox 2, same with Opera 9.1, but look at this mess in Internet Explorer 7, during the drag process:

Copyright © John Leach 2008. All rights reserved.

41


The positioning has clearly gone horribly wrong. The item being dragged seems to move under the done list, and script.aculo.us hasn't done its usual magic shifting the correct red items out of the way. The item actually ended up in the right list, more or less in the right position. Sigh. Do we want to find the culprit? I think that would take a lot more work, though my money's on that particular browser. Of course, the others have their problems too. This is, in my humble opinion, a fairly unlikely situation, so I'm going to pretend that it doesn't happen. Life is full of compromises.

Final Version The final step is to fix the first <li> problem, remove our fixed list from the XHTML markup, and build it from a JavaScript object. This is a slightly convoluted exercise, but allows us to experiment with dynamic DOM manipulation ourselves. To do this, we'll have to modify our little trio; files.

lists.js, lists.css

We'll start with the lists.html file, and fix our first Only the body changes, as follows: 13 14 15 16 17 18 19 20 21 22 23 24

42

<li>

and lists.html

problem at the same time.

<body> <p class='title'>Example of prototype/script.aculo.us usage</p> <div id='todo'> <ul id='todoList'> <li class='fixed'>Todo List:</li> </ul> </div> <div id='done'> <ul id='doneList'> <li class='fixed'>Done List:</li> </ul> </div>

Copyright Š John Leach 2008. All rights reserved.


25 <pre id="feedback">...</pre> 26 </body> 27 </html>

Basically what we are doing here is to remove the lists (they'll be created dynamically by our JavaScript), but more importantly, we've moved the list titles into the lists themselves. This way, we'll have a fixed first item, which cures the first item problem nicely. Next we'll look at our lists.css file, again I'll just highlight the changed style rules: 18 19 20 21 22 23 24 25 26 27 28 29 30

ul { list-style: none none; font-size: 11pt; margin: 0.2em; padding: 0.2em; } li { border: 1px solid #000000; margin: 0.2em; padding: 0.2em; }

Our first two style rules define the styles for generic <ul> and <li> elements. The only real difference is that we've added a little margin and padding to the <ul> element. 31 32 33 34 35 36 37 38

li.fixed { font-size: 12pt; } li.sortable { cursor: move; }

We have two types of sortable list). 39 40 41 42 43 44 45 46

<li>

element;

fixed

(for the titles) and

sortable

(for the

#todoList li.fixed { color: #bc2c2c; } #doneList li.fixed { color: #2cbc2c; }

These two style rules set the font colour for the titles of each list. 47 48 49 50 51 52 53

#todoList li.sortable { background-color: #ecacac; } #doneList li.sortable { background-color: #acecac; }

The last two style rules set the background colours for the sortable items of each list. A Touch of JSON

We'll finish our final example by modifying the

lists.js

file.

Add the following lines: Copyright Š John Leach 2008. All rights reserved.

43


07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23

doneList: [1, 2, 3], todoList: [4, 5, 6, 7, 8, 9, 10], items: { 1: "1. Welcome and Software Installation Lesson", 2: "2. JavaScript Basics Lesson", 3: "3. More Advanced JavaScript Lesson", 4: "4. Using Professional JavaScript Libraries", 5: "5. JavaScript Refresher Lesson", 6: "6. Ruby Basics Lesson", 7: "7. Ruby on Rails Basics Lesson", 8: "8. Ajax Basics Lesson", 9: "9. Ruby Refresher Lesson", 10: "10. Party Time, Questions and Answers" },

Here we create three objects; two arrays and a map, which are also JSON objects. This will come in handy when we start working with Ajax and a back-end server, but that's still a little way off. There isn't anything special here, the two arrays hold the identifiers of the items done, and the identifiers of the items to do, respectively. The map holds all the items, and uses the identifier as the key. Think of this as a database table, or spreadsheet, where the identifier value represents the database record number, or row number of the spreadsheet. Data Synchronisation

Previously, we had a updated() method which was called when the list order was changed, or items were added or deleted from the list. At the time, it didn't do very much, but now it's going to become a key component, because it will be responsible for keeping our data model synchronised to the view model that the browser user sees. So what will we have to do to synchronise the data? We'll have to modify the ordered list of identifiers on every updated() call. 24 25 26 27 28 29 30 31 32 33 34 35 36

updated: function (list) { var id = list.getAttribute('id'); var array = SortableLists[id]; array.length = 0; Sortable.sequence(list).each(function (value) { array.push(parseInt(value)); }); // Debugging code $('feedback').innerHTML = id + "<br /> " + " todo: " + SortableLists.todoList.inspect() + "<br /> " + " done: " + SortableLists.doneList.inspect(); },

Our updated() method expects the list element as its only parameter. It gets the identifier of the list, and then gets the associated ordered array for that list (lines 25 to 26). Then it recreates the ordered list, by walking though the <li> elements given by the Sortable.sequence() script.aculo.us method, which returns all valid identifiers as strings. To get the data model identifier then, we simply convert the identifier to an integer and add it to the ordered array (line 29). 44

Copyright Š John Leach 2008. All rights reserved.


The debugging code simply creates a string for the values of the todo and the done ordered lists.

list

being modified, and the

Obviously, once we're convinced that the code works, we can comment out lines 32 to 34. Using

Sortable.sequence()

We should be a little careful here, because Sortable.sequence() is an internal method, and is not documented in the Wiki. It could change in future versions of script.aculo.us, breaking our code. Caveat Emptor. Finally, we modify our existing createSortables() method so that it creates the lists first. 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

createSortables: function (event) { var lists = SortableUtils.lists; // create the lists lists.each(function (name) { var list = SortableUtils[name]; var container = $(name); var attrs = { id: 'todo', 'class': 'sortable' }; list.each(function (id) { var text = SortableUtils.items[id]; attrs.id = 'todo_' + id; container.insert({ bottom: Builder.node('li', attrs, text) }); }); });

The interesting part here is on lines 40 to 49, which creates the lists in the DOM. First we create a set of <li> elements for each list (lines 44 to 48), using our now familiar each() method. The script.aculo.us library provides a helper object Builder, which creates DOM nodes for us. All we have to supply is the node name, a map of attributes, and the text to go into the node as contents (line 47). We then insert this newly created node into the <ul> element (container), using the Prototype Element.insert() method. Note that, since the element must go to the bottom of the list, we specify the bottom parmeter. 52 // make them sortable 53 lists.each(function (id) { 54 Sortable.create(id, { 55 dropOnEmpty: true, 56 containment: lists, 57 constraint: false, 58 only: 'sortable', 59 onUpdate: SortableUtils.updated 60 }); 61 }); 62 } 63 };

The code to make our lists sortable has hardly changed. We have, however, added the only option, which tells script.aculo.us to only make <li> elements sortable that have the sortable class name. This will leave our fixed title <li> elements, er, fixed. Finally, fire up your favourite browser (Firefox, of course), and try it out. You'll see the debugging text at the bottom of the page, once you move an item. Check that it is correct. Copyright Š John Leach 2008. All rights reserved.

45


Source Files All the source files for this lesson, including the Aptana project file can be found in the LUGPC4.zip archived file, distributed under the GNU Lesser General Public License.

What's Next? We'll retrace our steps a little, to make sure that you understand these new concepts.

46

Copyright Š John Leach 2008. All rights reserved.


Review of JavaScript, CSS and XHTML LUG Programming Course, 11th February 2008 The last two lessons introduced us to XHTML, CSS and some pretty advanced aspects of the JavaScript programming language. This lesson will reinforce our knowledge of these topics, mostly by providing links to articles and short tutorials available on the Internet, and answer some of the student's questions. This time I managed to get though the lesson with about 30 minutes to spare. I think that some of the information given here helped clear up some of the doubts that the students had. We spent the last 30 minutes looking at specific problems, and discussing students interests and aims.

XHTML Review All in all, XHTML is about the easiest concept to understand. We've seen that we can create content using elements, and that those elements are roughly divided into block types and inline types. Only block types modify the flow of the text. Rather than go over well trodden territory, I'll simply provide some links for further reading: •

W3Schools XHTML Reference

Learning XHTML

A summary of XHTML

Why tables for layout is stupid, disponibile anche in italiano.

Certainly, one cause of head scratching was the Document Object Model, or DOM. This is the actual representation used by the browser of the document after it has been parsed. It is what I refer to when applying style rules, and for accessing elements in JavaScript. The W3Schools tutorial also provides graphical representations of the DOM. Mozilla also provides some information in italiano. Another way of viewing the DOM is by using the View Source Chart Firefox add-on. There is also a small cheat sheet available, in PDF format.

CSS Review Up to now we have breezed through CSS in a rather superficial manner. CSS has a simple syntactical format, at least compared to JavaScript, and a ton of properties which allows us to radically change the presentation of the document, while leaving the content to XHTML. Of course, you can do some pretty exciting things with CSS these days – one of my favourite sites is the CSS Zen Garden. A List Apart is another good site for CSS amongst other things. They have an interesting article on lists, which would probably make last week's example a little more lively. When you're totally confused by a selector such as

body > h2:not(:first-of-

Copyright © John Leach 2008. All rights reserved.

47


type):not(:last-of-type),

then just plug it in to the definition in plain English, or Spanish if you prefer.

selectoracle

and you'll get a

An introductory spiegazione di CSS in italiano is provided by Mozilla. Take a look at Eric Meyer's CSS2 test suite, and his technical articles. Of course, there's a cheat sheet, in PDF format. Finally, don't forget that Firefox/Firebug allow you to temporarily disable any style rule:

Here I've disabled the web site.

background

property for the contents/tagcloud areas on my

JavaScript Review We've seen how to create variables and functions, looked at expressions and statements, and how to create objects. We've also made use of some sophisticated libraries to improve our efficiency. But some of us came unstuck when we met things like anonymous functions. Truth

Quick â&#x20AC;&#x201C; what's true in JavaScript? Well, it's everything except the empty string "".

null, undefined, 0,

or

Types

We've seen numbers and strings, but remember that functions are also a type. In fact, we've also seen arrays and maps (though these are really just objects). 48

Copyright Š John Leach 2008. All rights reserved.


For the more adventurous, there is also the expressions), which we'll look at again next.

date

type, and

regexps

(regular

Regular Expressions

Always a tricky subject, yet so powerful when you get the hang of it. Russ Olsen has written a short ten step guide to using regular expressions. You can try the JavaScript regular expression cheat sheet, in PDF format. Anonymous Functions

This did cause some problems, so I'll try to give some examples to make things clearer. Firstly, let's look at the traditional syntax of functions, compared to variables (you can try these examples for yourself in Firebug): var value = "bee ant wasp spider fly"; function toSortedArray(list) { return list.split(' ').sort(); } var result = toSortedArray(value); result

which gives: ["ant", "bee", "fly", "spider", "wasp"]

So, we have two variables value and result, and a function toSortedArray. The function returns a sorted array of the words given in the parameter list. I know that we could eliminate the function in this example, and just call split(' ').sort() directly on the string. Imagine that the function body did something more complicated, such as making the first character of each array element uppercase. Now, unless value is going to be used somewhere else, we didn't really need to create a variable for it: function toSortedArray(list) { return list.split(' ').sort(); } var result = toSortedArray("bee ant wasp spider fly"); result

also gives: ["ant", "bee", "fly", "spider", "wasp"]

It is normal programming practice to avoid using unnecessary variable names, as this makes the code more concise. So the string "bee ant wasp spider fly" has become anonymous. If you type in: typeof toSortedArray

(note I deliberately did not add the parentheses), you'll get this: "function"

Which means that toSortedArray is a variable of type function. If we can reference our function toSortedArray in the same way we would a variable, then a Copyright Š John Leach 2008. All rights reserved.

49


function can also be defined in the same way as for a variable, as follows: var anotherToSortedArray = function (list) { return list.split(' ').sort(); }; var result = anotherToSortedArray("bee ant wasp spider fly"); result

which gives the same result as before: ["ant", "bee", "fly", "spider", "wasp"]

In addition, we can ask for information about this variable as we did for toSortedArray: typeof anotherToSortedArray

also gives the result: "function"

So, if we made our value variable anonymous (because it wasn't needed anywhere else), can we do the same for the anotherToSortedArray function (if it isn't needed anywhere else, either)? Yes we can, as long as we remember to call it immediately after the definition: var result = function (text) { return text.split(' ').sort(); }("bee ant wasp spider fly"); result

which gives the same result as before: ["ant", "bee", "fly", "spider", "wasp"]

Although this technique might seem somewhat convoluted to you, it does have its uses, and you'll find it used in the Prototype library. I will conclude this discussion by creating a function to sort our list by length, rather than by alphabetical order. Here is the code, using a named function sortByLength: var sortByLength = function (lhs, rhs) { return lhs.length - rhs.length; }; var toSortedArray = function (list) { return list.split(' ').sort(sortByLength); }; var result = toSortedArray("bee ant wasp spider fly"); result

which gives the result: ["fly", "ant", "bee", "wasp", "spider"]

Assuming that sortByLength is not required elsewhere, we can remove it completely by passing the anonymous function definition directly to the sort method: var toSortedArray = function (list) { return list.split(' ').sort(function (lhs, rhs) { return lhs.length - rhs.length; }); }; var result = toSortedArray("bee ant wasp spider fly"); result

which, of course, gives the same result: ["fly", "ant", "bee", "wasp", "spider"]

50

Copyright Š John Leach 2008. All rights reserved.


Unit Testing

If you've spent any time looking at the script.aculo.us library that we downloaded for the previous lesson, you may have noticed the test folder. Inside that folder you will find a unit folder, which contains several HTML files. Try launching the string_test.html file, the result of which should look something like this:

So what's that all about? Well, it is a set of tests for the additional string methods provided by script.aculo.us. This technique is called unit testing, and provides several advantages, both for the user and for the programmer. Of course, any software that is executed will be tested – usually by the user of that software. However, it is much better if the software is tested before being delivered to the user. As a programmer, I need to have confidence in the software that I write, and also in the software that I use, such as the Prototype and script.aculo.us libraries. Although not infallible, unit tests show us that the software developer has taken care to ensure that the software actually works. By supplying a suite of unit tests we can check that the software is still working, both today and tomorrow. If that sounds a little strange, let me explain why the software might stop working. We might actually make a change to our software, some little aesthetic modification, which shouldn't cause any problems. This usually leads to the immortal cry of “But I didn't change that code!”. In the case of JavaScript, we might simply change the browser, either by using a different browser, or by upgrading the version of our favourite browser. In other languages, we might upgrade or change the operating system, or upgrade the language itself. Additionally, we might have upgraded one or more of the libraries we are using. In any or all of these cases, we can't be sure that our software still works correctly. To check that your software (still) works in these new conditions, the unit tests can be run again (and again). It's like wearing a safety belt in your car – it won't help in every circumstance, but it's a lot better than not wearing one at all. Your First Unit Test

For this lesson, we'll create a set of unit tests for some of the code we produced in the previous lessons. The source files are given at the end of this section. First, let's look at our code in experimental.js:

Copyright © John Leach 2008. All rights reserved.

51


01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

/* * Vowel count (debug) exercise */ function countVowels(str) { var result = str.match(/[aeiou]/ig); return (result) ? result.length : 0; } /* * JavaScript objects exercise */ String.prototype.squash = function () { return this.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' '); }; var Validator = { // Returns the string, or null if not numeric numeric: function (str) { str = str.squash(); var result = (/^\d+$/).exec(str); return (result) ? result[0] : null; }, // Returns the string, or null if not a valid email email: function (str) { str = str.squash(); var result = (/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i).exec(str); return (result) ? result[0] : null; } };

There's nothing exciting to see here. It's basically just the code we wrote a couple of lessons back, with the console.log() statements removed. They were performing a sort of home made unit test, but now we can utilise something better. Next, we'll use the script.aculo.us unit testing framework to create our own unit tests for our code. The XHTML page, unittest.html, is pretty straightforward: 01 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 02 <html xmlns='http://www.w3.org/1999/xhtml'> 03 <head> 04 <meta http-equiv="Content-Type" content='text/html; charset=UTF-8' /> 05 <link rel="stylesheet" href="styles/test.css" type="text/css" media="screen" charset="utf-8" /> 06 <script src="scripts/prototype.js" type="text/javascript" charset="utf-8"></script> 07 <script src="scripts/unittest.js" type="text/javascript" charset="utf-8"></script> 08 <script src="scripts/experimental.js" type="text/javascript" charset="utf-8"></ script> 09 <title>LUG Programming Course, Lesson 5</title> 10 </head> 11 <body> 12 <h1>LUG Programming Course Unit Tests</h1> 13 <p>Tests for objects and extensions written in lesson 3</p> 14 <!-- Test Log output --> 15 <div id="testlog">...</div> 16 <script src="scripts/tests.js" type="text/javascript" charset="utf-8"></script> 17 </body> 18 </html>

The unittest.js file, linked on line 7, comes directly from the script.aculo.us download. The only difference is that I have linked to our tests.js file, on line 16, rather than include the testing code directly in the HTML page, as script.aculo.us does. Line 15 is the placeholder for the unit test results, which is very similar to what 52

Copyright Š John Leach 2008. All rights reserved.


we used for our Logger class. Finally, we can take a look at the unit testing code itself, in the tests.js file: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

/* * Unit tests for the code developed in lesson 3. */ new Test.Unit.Runner({ testCountVowels: function () { this.assertEqual(2, countVowels("Hello")); this.assertEqual(0, countVowels("Krk")); }, testSquash: function () { this.assertEqual("Goodbye Cruel World", "\n\t\u00a0Goodbye \t\u00a0 Cruel \t\u00a0\t\u00a0 World\n\t".squash()); this.assertEqual("", "\n\t\u00a0 \n\t\u00a0\n".squash()); }, testValidator: function () { this.assertEqual("000123", Validator.numeric(' 000123 this.assertNull(Validator.numeric(' 000123x ')); this.assertNull(Validator.numeric(' '));

'));

this.assertEqual("mickey_and_minny@mouse.co.uk", Validator.email(' mickey_and_minny@mouse.co.uk ')); this.assertEqual("mickey_and_minny@mouse.info", Validator.email(' mickey_and_minny@mouse.info ')); this.assertEqual("mickey_and_minny@mouse.com", Validator.email(' mickey_and_minny@mouse.com ')); this.assertNull(Validator.email(' mickey@minny@mouse.co.uk ')); this.assertNull(Validator.email(' mickey_and_minny @ mouse.com ')); this.assertNull(Validator.email(' mickey&minny@mouse.com ')); this.assertNull(Validator.email(' mickey_and_minny@mouse.information

'));

} });

So what happens here? The Test.Unit.Runner object receives our (anonymous) test object, and executes all the methods that it finds that begin with test. In our case there are three of them. Each test method performs a series of checks, called assertions, to ensure that our code works correctly. The script.aculo.us unittest.js file provides several assertion methods (lines 285 to 433), which allow us to test if the expected result is given by our software. For our tests, I've made use of just two methods; assertEqual and assertNull. The first method reports a passing test if the expected value is equal to the computed value, the second method reports a passing test if the computed value is null. Obviously, any other result will cause the tests to fail. There is an optional parameter (which we haven't used in our examples) which allows us to specify a message if the test fails. The result is the rather satisfying unit test page:

Essentially, all we have done is to remove our original tests, and put them inside a Copyright Š John Leach 2008. All rights reserved.

53


more sophisticated framework. However, we have also separated the 'test' code from the 'work' code. In addition, we are providing a documented, repeatable test environment, which can be used at any time, now or in the future. Of course, if a bug should appear in the future, such as occurred with our countVowels() function, you can add a (currently failing) test, and then modify the code until the test passes, while ensuring that all the other tests continue to pass.

Source Files All the source files for this lesson, including the Aptana project file can be found in the LUGPC5.zip archived file, distributed under the GNU Lesser General Public License.

What's Next? We'll move on to the Ruby programming language.

54

Copyright Š John Leach 2008. All rights reserved.


Ruby Full Immersion LUG Programming Course, 18th February 2008 This week we move on from JavaScript to the Ruby programming language. Ruby is a dynamically typed, interpreted programming language. It's available for the Windows, Mac OS X, and Linux operating systems. This lesson will give a very brief overview of the language, noting important differences from JavaScript where necessary. The first, and probably most important difference from JavaScript, is that Ruby runs on the operating system itself, rather than within a browser. Ruby can be used to create classic glue language scripts, much like AWK or Perl. It can also be used to create desktop applications, using a variety of bindings to underlying libraries, such as Fox, wxWidgets, Korundum and Qt, and Tk. You can also collect your Ruby scripts together to make a stand alone executable using RubyScript2Exe. Undoubtedly, Ruby on Rails has made Ruby widely known as a web application programming language. Il sito Ruby Italia fornisce guide ed altri informazioni in italiano. There is a wealth of documentation available, including the core and standard libraries. There is an online interpreter and tutorial, which is a clever idea indeed. I managed to cover the material in the allotted time frame, but I'm still leaving a few students behind. The part about iterators and code blocks left a lot of confused looks, much as it did in JavaScript. I'll save our 'bonus' lesson to go over this ground again in a couple of weeks.

Ruby Basics You can try out the following examples using the Interactive Ruby Shell called irb. Just type irb at the command line, and type exit when you've had enough. Ruby also has ri the documentation reader. You can look up documentation for a given class, or method using this command line program. For example, to look up the String class type ri String. You can find out more about ri by typing ri --help. Ruby has several types, numbers, strings, arrays, hashes, ranges, symbols and regular expressions. Unlike JavaScript, Ruby has integer numbers as well as floating point numbers. The Bignum class can handle truly enormous numbers, which can be useful for astronomers and bankers. You can use the underscore (_) to separate big numbers, such as 35_000_000. Strings

are a little more tricky. Just like JavaScript you can use single quotes ( ') or double quotes ("), but single quoted strings only accept \\ and \' as escape sequences. Double quoted strings can accept a much richer set of backslash (\) sequences, as well as embedded expressions, for example "Your name is #{name}". You can use different single character delimiters using %q and %Q, and multi-character delimiters using here documents: %q{String definition}, Copyright Š John Leach 2008. All rights reserved.

55


%Q<String\tdefinition>,

or

str = <<xXx <!-- saved from url=(0014)about:internet --> <html xmlns="http://www.w3.org/1999/xhtml"></html> xXx

Arrays

have a similar syntax to JavaScript:

a = [ 'bee', 'wasp', 3.14159 ]

As with JavaScript, array elements are accessed by index value (in the range 0..array.length - 1), but Ruby adds a little magic by allowing negative index values, which refer to the end of the array. So a.last == a[-1] a[a.length â&#x20AC;&#x201C; 1] == a[-1]

both give the result: true

Hashes

(or associated arrays) have a slightly different syntax from JavaScript:

b = { 'butterfly' => 'float like a butterfly', 'bee' => 'sting like a bee' }

Hash elements are accessed by key: b['butterfly']

gives: "float like a butterfly"

Ranges are very useful for creating sequences: ('a'..'f').to_a

gives: => ["a",

"b", "c", "d", "e", "f"]

whereas (note there are three dots, not two as before): (0...10).to_a

gives: => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Symbols are unique strings, and are created by prefixing colon (:), such as :name, or :"Name is #{name}". They

the string or name with a are guaranteed to have the

same identifier: "hello".object_id == "hello".object_id

gives: false

Whereas: :"hello".object_id == :"hello".object_id

gives: true

Regular expressions also have a similar syntax as the %r alternative, re = %r{[aeiou]}. 56

to JavaScript, re

Copyright Š John Leach 2008. All rights reserved.

= /[aeiou]/,

as well


As you've probably guessed, a local variable starts with the variable name (it does not require a var or other keyword), and the semicolon statement terminator ( ;) is optional, when there is only one statement per line. Names can be letters followed by letters, numbers or the underscore. Naming style is different from JavaScript in that names – including filenames - are normally lowercase, and words are separated by underscores, for example word_count. Constants always start with an uppercase letter (and usually are all uppercase, such as PI), and class names follow the UpperCamelCase naming style. Comments start with a hash sign (#) and continue to the end of the line. Multiple line comments can be created between a line starting with =begin and a subsequent line starting with =end. For example: # normal comment a = 1 =begin a = 2 =end puts a

gives: 1

The boolean values are true and false, but a non existing object is nil, and not null as in JavaScript. Ruby has no equivalent of undefined. The nil object also has methods, so to test for nil (on any object) we can call obj.nil?, which only ever returns true if the object is nil. Ruby makes accessing operating system commands very easy using the back quotes (`) or using %x. For example, in irb, the following will print the list of files in the current working directory: puts `ls` # Unix puts `cmd /C "dir /B"` # Windows

A Ruby Program Dissected We'll create a short Ruby program to count the number of words in a file. To add a little spice, and to demonstrate some of the power of Ruby, we'll also calculate the unique word count. The text for this exercise comes from “The Happy Prince and Other Tales”, by Oscar Wilde, available from project Gutenberg. The code consists of a single class WordCount, and a small command line program which uses the class. Here then is the code in the word_count.rb file, with explanations (you can download the source files at the end of this discussion): 01 #!/usr/local/bin/ruby -w 02

For any script that will be run as a program, put a shebang on the first line. The -w option forces the Ruby interpreter to display warnings. Removing all warnings from our code allows us to write better idiomatic Ruby. The interpreter will tell us when we've got things wrong. Copyright © John Leach 2008. All rights reserved.

57


03 04 05 06 07 08 09 10 11 12 13 14 15 16

# # # # # # # # # # # # #

== Synopsis word_count: Counts the number of words in a file == Usage word_count [options] -- file_path -h, --help: show help -m n, --min n: minimum of n letters in the word (default is 2) file_path: the file path of the file to read the words from.

The header comment can be parsed by RDoc to produce documentation of our code. This can also be used inside the program itself, as we'll see a little later. 17 # The WordCount class calculates unique and word counts in 18 # a file. 19 class WordCount 20

This is the class definition, which is terminated by the end statement on line 62. A class can inherit methods and variables from another class, called the super class, by specifying < name after the class name. In our case we have not specified a super class, so Object (the base class in Ruby) is inferred. Line 19 is equivalent to class WordCount < Object. 21 22 23

# The minimum number of letters which make a word. MIN = 2

We define a class constant for the default minimum number of characters in a word. This can be referenced inside the class by the name MIN, and outside the class using scope resolution; WordCount::MIN. Since this is a constant it's value must be initialised. 24 25

attr_reader :file, :title, :min, :count, :word_hash

The Module.attr_reader method is a piece of Ruby magic designed to make our lives a little easier. It uses metaprogramming to produce an instance variable, and a read accessor method for file, title, min, count and word_hash. Also notice that line 24 is a method call. In Ruby the parentheses are optional in certain circumstances, particularly when calling a method in a simple statement, such as in this example. It is normal Ruby practice to leave off the parentheses, except where this would cause confusion. So line 24 is equivalent to attr_reader(:file, :title, :min, :count, :word_hash). Just taking the first parameter into consideration, like:

attr_reader

@file def file @file end

We'll look at what that means in a moment. 58

Copyright Š John Leach 2008. All rights reserved.

produces something


26 27 28 29 30 31 32 33 34 35

# Creates a new WordCount object for the given file name # and minimum word length. def initialize(file, min = 1) @file = file @min = min <= 0 ? MIN : min @count = 0 @title = nil @word_hash = Hash.new(0) end

Methods are defined using the def keyword, terminating with the end keyword (line 34). The initialize method is special in a Ruby class, because it is the constructor method. Every time a new WordCount instance is created, this method is called. Our initialize method expects two parameters; file and min (line 28). The second parameter, min, is optional, if it is not specified it will take the value of 1. Required parameters must appear before optional parameters. In the body of our method we initialise the instance variables @file, @min, @count, @title, and @word_hash. Instance variables have an @ prefix symbol. We use a ternary expression on line 30 to ensure that @min cannot be less than 1. Whereas in JavaScript new is a keyword, in Ruby new is a class method. So line 33 assigns a new Hash instance to the instance variable @word_hash. The argument value 0 is the value that will be assigned to each newly created element of the hash. 36 37 38 39

# Parses the file. def parse re = /\w{#{@min},}/ File.open(@file, "r") do |file|

The parse method does the real work. Since it takes no arguments we don't need to write the parentheses. On line 38 we create a local variable re and assign it to a regular expression. Inside the regular expression we can use an embedded expression to calculate the minimum number of occurrences of the word letters. If @min has a value of 2, then re, the regular expression becomes /\w{2,}/. We make good use of the Ruby File and String classes in our little program, and on line 39 we use the File.open class method to open the file for reading. It not only opens the file, but it also closes the file automatically at the end of the code block, on line 49. I'll leave the discussion of Ruby code blocks till a little later. 40 41 42 43 44 45 46 47 48 49

while line = file.gets line.strip! if line.length > 0 @title = line unless @title words = line.scan(re) @count += words.length words.each { |word| @word_hash[word] += 1 } end end end

Line 40 uses a while expression ... end code block to read each line of text. The while loop is terminated when the expression evaluates to false. The File.gets method either returns a string (the line of text) or nil if there are no more lines. In Ruby everything is true, except false and nil. So the while loop will terminate when no more lines of text are available. Copyright Š John Leach 2008. All rights reserved.

59


The File.gets method returns the line including end of line characters. We can remove these characters with the String.strip! method (line 41). In Ruby, methods which end with an exclamation mark (!) usually modify the object itself, rather than returning a modified object. Since we have quite a lot of work to do on the line, we use a classic 42 to only check lines which contain text.

if

test on line

Line 43 however, uses a statement modifier. Statement modifiers can be read as “execute the statement if the expression is true”. But unlike JavaScript, Ruby also offers a complementary test to if, called unless. So the statement modifier reads “execute the statement unless the expression is true”. In other words set the @title to line unless the @title has already been set - @title was set to nil in the initialize method on line 32. The actual word parsing is handled by the String.scan method which returns an array of words that match a string or regular expression (line 44). Now we get to another interesting Ruby construct, on line 46. We need to examine each scanned word, and add it to our unique @word_hash. This is handled by using an iterator method each. Iterators and code blocks are a fundamental construct of Ruby. An iterator, such as each in our case, is responsible for passing each element in its container to a code block that we supply (the code within the curly braces). The problem is that the iterator has to pass one or more parameters to the code block. Ruby handles this by allowing the code block to define an argument list using the vertical bar (|) to delimit the list. In JavaScript, if you remember, we had to use an anonymous function. So our code block on line 46 (which terminates with the closing curly brace on the same line) receives a single argument called word. An important point to note here is that the start of the code block - the do or opening curly brace ({) - must begin on the same line as the method's last parameter or closing parenthesis. In our case, there are neither, so we have to stay on the same line as the method call itself. In reality we've already seen another code block, on line 39. This code block uses the do ... end syntax, which is the preferred syntax in Ruby for multi-line code blocks. This is how File.open can guarantee that the file is closed when the code block terminates. How does that work exactly? I'll explain using pseudo-code: def File.open(name, mode, code_block = nil) file = _open(name, mode) code_block(file) if code_block _close(file) end

In Ruby, the code block can be passed to the method as the last parameter. This allows File.open to open the file, pass the open file to the code block (if there is one), and then close the file itself. At this point we have almost finished the parse method. 50 51 52

60

self end

Copyright © John Leach 2008. All rights reserved.


Although it doesn't need to return any value, it is normal Ruby practice whenever possible to return the object itself, as this allows us to use method call chaining. This is exactly what we do on line 50. In Ruby self refers to the current object, whereas in JavaScript we used this. Unless specified by using return explicitly, Ruby will always return the last expression executed in a method. So line 50 is equivalent to return self. 53 54 55 56 57

# The unique word count def unique @word_hash.length end

We've already provided read accessor methods to our instance variables, but we also need to provide a read accessor method for the unique word count. 58 # Returns the count summary. 59 def to_s 60 "File: #{@file} title: '#{@title}' min: #{@min} total: #{@count} unique: #{unique}" 61 end 62 end 63

Similarly, we use the “convert object to string” method, to_s, to return a summary of the result of parsing the file. This is extremely simple in Ruby through the use of embedded expressions. Line 62 brings us to the end of our class definition. 64 # Run the application. 65 if __FILE__ == $PROGRAM_NAME # aka $0 66

Next we'll write the code which will run our program. Our WordCount class could be used by other programs, so line 65 tests that the current file ( __FILE__) is also the program name ($PROGRAM_NAME or $0). If this is the case, the program code will be executed, otherwise it will be ignored. This test is used frequently in library code to execute tests when the file is run as a program. 67 68 69

require 'getoptlong' require 'rdoc/usage'

The Kernel module provides several helper methods, including require, which loads the specified script into memory, which the interpreter then parses. This method allows us to add code that we require, but that is not part of the core Ruby library. 70 71 72 73 74 75

min = WordCount::MIN path = nil opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--min', '-m', GetoptLong::REQUIRED_ARGUMENT ] )

Lines 72 to 75 make use of a Ruby class called GetoptLong which handles command line options in a uniform way. The format is well known in the Linux world, but may be new to Windows users. Each option can have two formats, long and short, possibly followed by an argument. 76 77 78

opts.each do |opt, arg| case opt when '--help'

Copyright © John Leach 2008. All rights reserved.

61


79 80 81 82 83 84 85

Rdoc.usage exit 0 when '--min' min = arg.to_i end end

Lines 76 to 84 handle the options received. Here again, we use an iterator and a code block. Inside the code block there is a case ... when ... end statement. This is similar to the JavaScript switch ... case statement. Irrespective of the option style used, GetoptLong aways returns the long version. Here we test for the two predefined options. If the user asked for â&#x20AC;&#x201C;-help we can make use of our documentation comments at the start of the file to explain what the user needs to do. This is achieved using the Rdoc.usage method on line 79. The Kernel module also provides an exit method which exits the program, returning an exit code, which we give as argument to the method (line 80). Programs that terminate successfully usually return positive values or 0, and use negative values to indicate that an error occurred. Line 82 converts the string argument to an integer. Note that if a non integer value is given, the value returned is 0. See ri String#to_i for more information. 86 87 88 89 90 91 92 93 94 95

if ARGV.length != 1 puts "Missing file_path argument (try -â&#x20AC;&#x201C;help)" exit(-1) end path = ARGV.shift unless File.exists?(path) && File.file?(path) && File.readable?(path) puts "File path #{path} doesn't exist, isn't a file, or can't be read" exit(-1) end

Lines 86 to 89 check that the user has specified a file path, and if not, exits after printing an error message. The command line arguments are stored in the ARGV array, and by the time GetoptLong has finished, we should be left with just the file path. The exit statement on line 88 uses parentheses, because without them Ruby generates a warning. Line 90 removes the file path name from ARGV and stores it in the local variable path. Line 91 checks that the path exists, is a file, and can be read, using the keyword. Just to remind you: 91

unless

unless File.exists?(path) && File.file?(path) && File.readable?(path)

could also be expressed as: 91

if !File.exists?(path) || !File.file?(path) || !File.readable?(path)

I hope you agree with me that the unless statement is more readable. 96 puts WordCount.new(path, min).parse.to_s 97 exit 0 98 99 end

Finally, we can get down to doing the job. Line 96 creates a WordCount instance, parses the text, and prints the summary. Because our parse method returns self, we can chain these three method calls (new, parse and to_s) together on one line. Again, this is normal practice in Ruby. 62

Copyright Š John Leach 2008. All rights reserved.


Now that we've been though the program code, let's see what it does. On the command line (and in the directory where word_count.rb resides), type: word_count -h

On Linux and Mac OS X, you will have to make word_count.rb executable, and possibly use the ./ prefix, as in ./word_count. The result should be something like: C:\Courses\LUGPC6\WordCount>word_count -h Synopsis -------word_count: Counts the number of words in a file Usage ----word_count [options] -- file_path -h, --help: show help -m n, --min n: minimum of n letters in the word (default is 2) file_path: the file path of the file to read the words from. C:\Courses\LUGPC6\WordCount>

Now type: word_count

the result should be something like: C:\Courses\LUGPC6\WordCount>word_count Missing file_path argument (try -–help) C:\Courses\LUGPC6\WordCount>

Next type: word_count -- ./text

the result should be something like: C:\Courses\LUGPC6\WordCount>word_count -- ./text File path ./text doesn't exist, isn't a file, or can't be read C:\Courses\LUGPC6\WordCount>

Now, we'll actually do some parsing, type: word_count -- ./text/TheDevotedFriend.txt

the result should be something like: C:\Courses\LUGPC6\WordCount>word_count -- ./text/TheDevotedFriend.txt File: ./text/TheDevotedFriend.txt title: 'The Devoted Friend' min: 2 total: 4154 unique: 926 C:\Courses\LUGPC6\WordCount>

Finally, let's set the minimum word length to 5. Type word_count -m 5 -- ./text/TheSelfishGiant.txt

and the result should be something like: C:\Courses\LUGPC6\WordCount>word_count -m 5 -- ./text/TheSelfishGiant.txt File: ./text/TheSelfishGiant.txt title: 'The Selfish Giant' min: 5 total: 518 unique: 253 C:\Courses\LUGPC6\WordCount>

Copyright © John Leach 2008. All rights reserved.

63


Feel free to use this code as a template for your own command line programs in Ruby.

Source Files All the source files for this lesson, including the Aptana Studio project file and test text files, can be found in the LUGPC6.zip archived file, distributed under the GNU Lesser General Public License.

What's Next? We'll take our knowledge of Ruby a step further by looking at a Ruby and Ajax example, creating our first web application.

64

Copyright Š John Leach 2008. All rights reserved.


Ruby and Ajax LUG Programming Course, 25th February 2008 After our brief, but intense, first look at Ruby, we can start using this excellent scripting language to create our first web application. We previously used the Prototype and script.aculo.us libraries to dynamically modify an XHTML page, now we'll use Ajax via the script.aculo.us Ajax.Autocompleter to 'talk' to a simple Ruby web application. This lesson will introduce you to WEBrick, the Ruby web server, HTML forms, the ERB templating system, and Ajax. Despite the amount of code involved, the lesson went quite quickly, and I managed to finish with about twenty minutes to spare. We used that time to go over some of the Ruby code again. Everyone seemed to enjoy the results of running their first web application. One student even started adding his own text files, which I found gratifying, because that was what the application was designed for – any number of plain text files.

The Word Search Web Application Well don't get too excited, this isn't going to get Google worried. Our objective in this lesson is to use the WordCount class from the previous lesson so that the user can type a word, and then choose one of the text files that contains that word. To make things a little more interesting, we want the results list to show the title and word count for each text file, we want to use Ajax to reduce the bandwidth to the server and increase responsiveness, and we also want to fall back to traditional HTTP requests and responses if JavaScript is not available. A Little Architecture

Before we dive into the code, we'll have to spend a little time thinking about how we want to accomplish this task. Obviously we need to learn how to use the Ajax.Autocompleter class in script.aculo.us. There is an example in the /tests/functional/ folder of the downloaded code, called ajax_autocompleter.html. Further we'll need to write some Ruby to create our server application. This means we'll need to learn a little about the WEBrick server, and Ruby's templating system, called ERB. For a brief overview of WEBrick, there's the Gnome's Guide to WEBrick, by Yohanes Santoso. The class documentation provides plenty of examples for ERB. Finally, our two paragraph specifications require a little more from our WordCount class than it currently handles – we need the count for a specific word. The WordCount class has this information, it's just that there's no method to get at it. We'll also need a class that parses all five text files, and which effectively becomes the search engine, and a class for the results. Copyright © John Leach 2008. All rights reserved.

65


Before we start our project, let's take a look at the folder layout: app/ public/ scripts/ styles/ text/

This is a little more complicated than our previous examples. We've added an app/ folder which contains our Ruby code. Under the public/ folder we've added a subfolder text/ containing the text files. Why go to all this trouble? The reason is security. We don't want a user to have access to our web application script files, only to the public static files, or to the results that our application sends back. Although our application will only be run localhost (this is to say, on our own computer), I'm trying to get you into the habit of keeping security in mind â&#x20AC;&#x201C; it's a big bad world out there. The Search Classes

We'll build our web application in two files, one for the server (server.rb), and one for the search classes (search.rb). Of course, we'll also be using our word_count.rb file from the last lesson, which we'll copy into the app/ folder. As always, you can download the source code for this lesson, via the link given at the end of this discussion. First then, let's take a look at our 'search engine' in search.rb. 01 02 03 04 05 06 07 08 09 10 11 12 13 14

require 'pathname' require 'word_count' # Add a method to our existing class class WordCount attr_accessor :url # Return the count for a specific word def word_count(word) (count = @word_hash[word]) ? count : 0 end end

We need to add a couple of methods to our original WordCount class. Since Ruby is a dynamic language, it allows us to reopen existing class definitions (including built-in classes like String), and make the necessary changes. Since these changes are specifically for this application, we prefer not to change the original code. Lines 1 and 2 use the Kernel.require method to load external scripts into memory. We'll see this in more detail in our server application code. Line 7 adds a (read and write) accessor method for the url of the text file. We'll need this, because the URL is different from the file path we use to parse the file. Lines 10 to 12 adds a new method word_count which returns the word count for a specified word. The ternary operator is used to ensure that a numeric value of 0 is returned when the @word_hash does not contain the word. 15 # Holds the result of a word search 16 class Result 17 18 attr_reader :url, :title, :count

66

Copyright Š John Leach 2008. All rights reserved.


19 20 def initialize(wc, count) 21 @url = wc.url 22 @title = wc.title 23 @count = count 24 end 25 26 # Compare two results for ordering 27 def <=>(other) 28 return @title <=> other.title if @count == other.count 29 other.count <=> @count 30 end 31 end 32

Next comes the Result class. It's job is to contain the result for each word found, that is to say the URL, title, and word count. Lines 27 to 30 define the comparison operator for this class. Ruby, different to JavaScript, allows for operator overloading. The <=> method (the comparison or 'spaceship' method) is used, among other things, by Array.sort. We can overload this operator to order our results by highest word count (line 29), or by title when the word counts are identical (line 28). 33 # The search engine 34 class SearchEngine 35 36 attr_reader :files 37 38 def initialize(path, remove, add) 39 @files = [] 40 path = Pathname.new(path) 41 path.each_entry do |file| 42 file = path + file 43 if file.file? && file.readable? 44 url = file.to_s 45 wc = WordCount.new(url, WordCount::MIN) 46 wc.parse 47 wc.url = url.index(remove) == 0 ? add + url[remove.length..-1] : url 48 @files << wc 49 end 50 end 51 end 52

The search engine has two important jobs. First it has to parse all the text files, then it has to produce results for a specified word. Lines 38 to 51 take care of the file parsing. To do that we make heavy use of the Pathname class, as well as our modified WordCount class. We check every file in the path (line 41), accepting readable files (line 43), which we put into a WordCount instance and parse (lines 45 and 46), and then add to the @files array (line 48). Line 47 massages the file path into the correct web application URL, removing the remove string from the start of the URL, and suffixing the add string. Using a range and array notation in a String, such as url[remove.length..-1], is the Ruby equivalent of the JavaScript substring method. 53 54 55 56 57

# Return an array of the matching results def search(word) results = [] if word.length > 0 @files.each do |wc|

Copyright Š John Leach 2008. All rights reserved.

67


58 count = wc.word_count(word) 59 results << Result.new(wc, count) if count > 0 60 end 61 end 62 results.sort 63 end 64 end

The search method produces an array of Result instances for the specified word. Line 57 to 60 retrieve the word count for each file parsed (as long as the word is not the empty string, tested on line 56). If the word count is greater than 0, then we create a new Result object, and add it to the array (line 59). The array is then sorted and returned to the caller (line 62). The Server Application

With our helper classes in place, we can now look at the server application itself, server.rb. 01 02 03 04 05 06

#!/usr/local/bin/ruby -w require 'erb' require 'webrick' require 'search'

While our program has immediate access to all the built-in classes of Ruby, we frequently need to use other library code stored in external files. Ruby provides a mechanism for loading external files, which may be Ruby code or compiled binary libraries, via the Kernel.require method. If you leave off the extension, Ruby will search its library paths first for files with the .rb extension, then binary files with .so or .dll extensions. It is normal Ruby practice to leave off the extension. The application's directory also forms part of the paths that Ruby searches for external files, which means that we can also easily include our own code, on line 5. 07 # The Search Web Application 08 class SearchServer < WEBrick::HTTPServer 09 10 def initialize(config = {}) 11 super(config) 12

We'll create a SearchServer class based on the WEBrick::HTTPServer class. We'll leave WEBrick to do most of the hard work, but we still need our own initialize method. Here we simply use more or less the same parameters as the super class, and then call the super class initialize method, on line 11. Now that we have initialised the super class, we can get on with our own initialisation. 13 # HTML template 14 @html = ERB.new <<-xXx 15 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 16 <html xmlns='http://www.w3.org/1999/xhtml'> 17 <head> 18 <meta http-equiv="Content-Type" content='text/html; charset=UTF-8' /> 19 <link rel="stylesheet" href="/styles/search.css" type="text/css" media="screen" charset="utf-8" /> 20 <script src="/scripts/prototype.js" type="text/javascript" charset="utf-8"></script> 21 <script src="/scripts/effects.js" type="text/javascript"

68

Copyright Š John Leach 2008. All rights reserved.


charset="utf-8"></script> 22 <script src="/scripts/controls.js" type="text/javascript" charset="utf-8"></script> 23 <script src="/scripts/search.js" type="text/javascript" charset="utf-8"></script> 24 <title>LUG Programming Course, Lesson 7</title> 25 </head> 26 <body> 27 <p class='title'>Example of Ruby and Ajax</p> 28 <form id='searchForm' action='/search'> 29 <p class='search'> 30 <label for="search">Search word:</label> 31 <input type="text" id="search" name="search" maxlength='20' value='<%= word %>' /> 32 </p> 33 <div id="results" class="autocomplete" style="display: block;"> 34 <p>Results:</p> 35 <%= links %> 36 </div> 37 </form> 38 </body> 39 </html> 40 xXx 41

Lines 14 to 40 creates our first template. This is practically identical to the normal static XHTML file contents, except for lines 31 and 35. There we have used the ERB expression delimiters <%= ... %>, to inject two Ruby variables, word and links. The first will contain the word submitted by the form, and the second will contain the results of the word search. Another novelty are the <form> and <input> elements (lines 28 and 31). The <form> element defines or contains all of the <input> elements which make up a parameterised request. When the form is submitted to the URL defined by action, the parameters are sent to the server as name-value pairs. 42 # HTML/Ajax template 43 @ajax = ERB.new <<-xXx 44 <ul> 45 <% results.each do |r| %> 46 <li><a href='<%= r.url %>' type='text/plain'><%= r.title %> (<%= r.count %>)</a></li> 47 <% end %> 48 <% if results.length == 0 && is_html %> 49 <li>No matches found, sorry.</li> 50 <% end %> 51 </ul> 52 xXx 53

This is the second, and more complex template in our application. The <% ... %> delimiters allow us to add Ruby statements inside the template. Lines 45 to 47 produce <li> elements for each of the Result objects. Lines 48 to 50 add a <li> element explaining that no matches were found, though this only happens if there are no results and the is_html flag is true. In fact, we'll use this template both for the Ajax responses and for the HTML form submission responses. In the first case, we don't need to make any apologies, as the list will simply disappear, but in the case of our HTML form, we have already produced a “Results” title for our list, so we have to show something.

Copyright © John Leach 2008. All rights reserved.

69


54 55 56 } 57

# Search engine initialisation @engine = SearchEngine.new('../public/text', '../public', '') @engine.files.each { |file| logger.info("Loaded #{file.to_s} URL: #{file.url}")

Now we can build the search engine. We set the remove parameter to '../public' and the add parameter to '', so that all the text file URLs will map to '/text/...'. Just to make sure, we log the parsed results, so we can see that the right thing is happening via the server console on line 56. 58 59 60 61 62 63

# Define the server mount points mount('/', WEBrick::HTTPServlet::FileHandler, '../public') mount_proc('/search') { |req, resp| search(req, resp, true) } mount_proc('/word') { |req, resp| search(req, resp, false) } end

We use the mount method to map the public/ folder containing our static files. This tells the server that all server requests beginning with '/' should be handled as files, and that the FileHandler should look in the folder '../public'. We use the mount_proc method to specify the procedure or code block that responds to a specific server URL. Here we map the HTML form URL (/search) and the Ajax request URL (/word) to a search method that we'll define in a moment. Note that, for HTML form requests, the third parmeter is true, and for Ajax requests it is false. 64 # Define the dynamic (HTML form or Ajax) word search response 65 def search(request, response, is_html) 66 word = request.query['search'] || '' 67 word.strip! 68 results = @engine.search(word) 69 response['Content-Type'] = 'text/html' 70 links = @ajax.result(binding) 71 if is_html 72 response.body = @html.result(binding) 73 logger.info "HTML Word: '#{word}' Results: #{results.inspect}" 74 else 75 response.body = links 76 logger.info "Ajax Word: '#{word}' Results: #{results.inspect}" 77 end 78 end 79 end 80

In the search method the is_html flag is used to modify the response, either sending back the XHTML fragment using the @ajax template, when is_html is false, or the entire XHTML document using the @html template, when is_html is true. We retrieve the word from the request query, using the short circuit technique to guarantee a string value (line 66). The parameter name, search, matches the <input> element name attribute value. We remove any leading and trailing whitespace, and then call the search @engine for the search results on line 68. Next, we prepare the response, and use the Kernel.binding method to bind our local parameters and variables (is_html, word, and results) to the @ajax template. Calling the template result method returns a String, with all the expressions resolved. 70

Copyright Š John Leach 2008. All rights reserved.


Next we test if the is_html flag is true on line 71. If it is, we use the @html template (which incorporates the @ajax template result via the links local variable) to create the response.body, otherwise the response.body will consist of the result of the @ajax template, previously stored in the links local variable. In both cases, to be sure that the engine is doing the right thing, we send the results to the server logger (line 73 and 76), using the Object.inspect method to expand the contents of the array. 81 82 83 84 85 86 87 88 89 90

# Create the server server = SearchServer.new(:Port => 8086) # trap signals to invoke the shutdown procedure cleanly ['INT', 'TERM'].each do |signal| trap(signal) { server.shutdown } end # Start the server server.start

Finally we create the server, specifying port 8086, instead of the usual port 80, to avoid collisions with other web servers on your computer, such as IIS or Apache. Then we take a code snippet from the Gnome's Guide to WEBrick to trap shutdown (Ctrl+C), and start the server. The JavaScript File

Thanks to the all the code we have written in Ruby, and more importantly, to the script.aculo.us library, the JavaScript code in search.js is simplicity itself. 01 /* 02 * Word Search 03 */ 04 05 document.observe('dom:loaded', function() { 06 new Ajax.Autocompleter('search', 'results', '/word', { 07 method: 'get', 08 minChars: 2, 09 updateElement: function(item) { /* no update */ } 10 }); 11 });

We must specify the input element (search), output element (results), and the submission URL (/word). The important options here are minChars and updateElement. We set minChars to 2 because that is also the minimum word length that we parsed in the server. Normally Ajax.Autocompleter will fill the form input element with the value selected from the autocompletion list. In our case this isn't either necessary nor desired. Our list contains links to the text files, which we expect the user to click. To remove this default behaviour, we provide an empty function for updateElement. The CSS and HTML Files

The CSS file, search.css, is relatively simple, as shown below. 01 body { 02 background-color: #d8ffd8; 03 font: 12pt Verdana, Helvetica, sans-serif; 04 } 05

Copyright Š John Leach 2008. All rights reserved.

71


06 07 08 09 10 11 12 13 14 15

p, div, ul, li { margin: 0.1em 0.25em; padding: 0; } input { font-family: 'Courier New', Courier, monospace; width: 35ex; }

The first section sets style rules for the elements in the page. In particular, we set a monospace font for the <input> element. 16 17 18 19 20 21 22 23

.title { font-size: 14pt; } .search { margin: 1.0em 0.25em; }

We use two style classes for the page title, and to distance the input field from the title and the results list. 24 .autocomplete { 25 background-color: #d8f8d8; 26 border: 1px solid #4c4c4c; 27 width: 40ex; 28 } 29 30 .autocomplete ul li { 31 font-size: 10pt; 32 list-style-type: none; 33 margin: 0.25em; 34 } 35 36 .autocomplete ul li:hover { 37 background-color: #e8f8e8; 38}

The final section of our style sheet defines the appearance and behaviour of the autocompletion list. Our application actually 'lives' at the URL /search, but our users will probably be more used to simply typing the root URL /, which WEBrick converts into a request for /index.html. This means that we also need to supply a simple static XHTML file which redirects the user to our application: 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 <html xmlns='http://www.w3.org/1999/xhtml'> 3 <head> 4 <meta http-equiv="refresh" content="0;url=/search" /> 5 </head> 6 <body></body> 7 </html>

The redirect is actioned by the <meta> element on line 4. This causes the page to be refreshed after 0 seconds, using the URL /search.

Running the Application. To be able to run the application we need to open a console in the apps/ folder. Start 72

Copyright Š John Leach 2008. All rights reserved.


the server by typing server.rb. At this point we can reference the root URL of our application in a browser by typing http://localhost:8086/. If all goes well, you should see something like this:

Now type in flowers in the input box, and you should see the following:

The console output will also look like the following picture. Note the 'Ajax Word ...' log messages, which refers to data sent in response to the Ajax requests.

If you type

flowers

reasonably slowly, you will see the list appear once you've typed Copyright Š John Leach 2008. All rights reserved.

73


flow,

it then disappears for flowe and reappears for flower and flowers.

Functional Degradation

The specifications also required that our application still work for browsers that have disabled JavaScript, or that have no JavaScript interpreter (such as some mobile telephone browsers). Switch off JavaScript in your browser, then refresh the page â&#x20AC;&#x201C; you may want to clear the cache first. The result should be:

Now type flowers in the input box, then press Enter. The result should be:

As you can see from the picture above, the value of the input element search now also appears in the page URL. The console output will look like the following picture. Note that the log now shows only 'HTML Word ...' messages, as no Ajax requests can now be sent.

74

Copyright Š John Leach 2008. All rights reserved.


Source Files All the source files for this lesson, including the Aptana project file, can be found in the LUGPC7.zip archived file, distributed under the GNU Lesser General Public License.

What's Next? We'll take a pause to review some of the important features of Ruby, and take a high level look at Ruby on Rails.

Copyright Š John Leach 2008. All rights reserved.

75


Review of Ruby, Overview of Ruby on Rails LUG Programming Course, 3rd March 2008 The last two lessons presented a whirlwind guide to Ruby, demonstrating a simple command line program, and a simple web application. That was a lot of material to digest, so in this lesson we'll make a short review of the important things we learned, and explain iterators and code blocks in a little more detail, since they caused some problems for some of the students. As we still have much ground to cover, half of this week's lesson is also dedicated to an overview of the Ruby on Rails framework, which we'll be using in the next two lessons to construct a more complicated web application. The lesson went reasonably well. I overran by about 15 minutes. I'll be overrunning a lot more in the next two lessons, as there's much more to go though. Seems I haven't lost anyone permanently from the course â&#x20AC;&#x201C; they all diligently show up , some even and smile occasionally.

Ruby Review is a message passing object oriented language. Because it is message passing, objects can only communicate between themselves using methods. Everything in Ruby is an object. Ruby

Within a class, there are instance @variables, and class @@variables. If we want the rest of the (Ruby) world to access them, however, we have to write accessor methods, or use the attr_accessor helper methods to create them. In Ruby, only nil and false are false, everything else is true. The opposite of if is unless. Both can be used as statement modifiers, for example; @volume = 5 unless @mute. Ruby allows you to write more concise code by making the statement terminator (;) and parentheses (()) in methods optional in certain conditions. This is used frequently in Ruby code. Ruby has fewer keywords than other languages, preferring method calls in most circumstances. Classes are defined by class ... end code blocks, and methods are defined by def ... end code blocks. Iterators and Code Blocks - One More Time

Probably the best way to explain iterators and code blocks is to use a couple of examples. First we'll use some 'experimenting' code to dissect Ruby code blocks and how they get used, then we'll look at a more pragmatic example, creating our own iterators. Here is the code_block.rb code, with explanations: 01 #!/usr/local/bin/ruby -w 02 03 def always_twice

76

Copyright Š John Leach 2008. All rights reserved.


04 05 06 07 08 09 10 11 12 13 14

puts "In method 'always_twice' (line 4)" yield yield end puts "Start (line 9)" always_twice do puts "In 'always_twice' code block (line 11)" end puts "End (line 13)"

We have a method definition, always_twice on lines 3 to 7, followed by a method call with associated do ... end code block, on lines 10 to 12, all sprinkled with puts statements so that we can see what is happening. Let's look at the output: C:\Courses\LUGPC8\Review>code_blocks.rb Start (line 9) In method 'always_twice' (line 4) In 'always_twice' code block (line 11) In 'always_twice' code block (line 11) End (line 13)

We use do ... end code blocks here so that each puts is on a separate line – this is equivalent to using curly braces ({}). As you probably expected, we see Start..., In method..., and End..., but we also see In ... code block... twice. So each time we use yield in the method (lines 5 and 6) the code block is executed. Although yield looks like a method call, it is actually a statement. Its meaning is “hand over control to the method's associated code block”. Now we'll continue this little code blocks exercise, as follows: 15 16 17 18 19 20 21 22 23 24

def never puts "In method 'never' (line 16)" end puts "Start (line 19)" never do puts "In 'never' code block (line 21)" end puts "End (line 23)"

Again, let's look at the output, then we'll work out what happens: Start (line 19) In method 'never' (line 16) End (line 23)

In this second example, even though we have defined an associated code block for the never method call (lines 20 to 22), it is never executed. In fact, the never method has no yield statement (lines 15 to 17). In other words, the associated code block is completely under control of the method. The method can choose to execute the code block any number of times - including zero. One last exercise, this time we'll communicate some simple information to the code block: 25 26 27 28 29 30 31

def maybe(&block) puts "In method 'maybe' block? #{block ? 'true' : 'false'} (line 26)" yield "#{block.inspect}" if block end puts "Start (line 30)" maybe do |info|

Copyright © John Leach 2008. All rights reserved.

77


32 33 34 35 36 37 38

puts "In 'maybe' code block: #{info} (line 37)" end puts "End (line 34)" puts "Start (line 36)" maybe puts "End (line 38)"

Here's the output: Start (line 30) In method 'maybe' block? true (line 26) In 'maybe' code block: #<Proc:0x02ae593c@C:/Courses/LUGPC8/Review/code_blocks.rb: 31> (line 37) End (line 34) Start (line 36) In method 'maybe' block? false (line 26) End (line 38)

This time we've written a method, maybe, on lines 25 to 28, which defines the code block as a parameter. Although the parameter name, block, can be any name we choose, it must be prefixed with an ampersand (&), and it must appear as the last parameter in the parameter list. Different to our previous examples, the yield statement passes a string to the associated code block – if there is one. In the first test, lines 30 to 34, we call maybe with an associated code block, and that associated code block receives the string that maybe provides as a local variable info (line 31). In the output, you can see that Ruby converted the code block into a Proc object. In the second test, we call maybe with no associated code block. Since block is nil inside the method, yield is not executed. Attempting to execute yield where there is no associated code block causes the interpreter to produce an error. Popular Authors and Iterators

Now that we've seen what can be achieved with methods, associated code blocks, and the yield statement, we can create our own iterator. I'll use the Project Guttenberg “Top 100” page as data for this example. This lists the top 100 books and authors, by number of downloads over the last few days. Actually, I'll just use the top 10, to keep things simple. Here is the authors.rb code, with explanations: #!/usr/local/bin/ruby -w # Top 10 Authors (from http://www.gutenberg.org/browse/scores/top) # Deliberately not in alphabetical order authors = { "Shakespeare, William" => 31758, "Austen, Jane" => 39508, "Verne, Jules" => 24615, "Dickens, Charles" => 37563, "Tzu, Sun" => 22447, "Thomson, J. Arthur" => 36825, "Doyle, Arthur Conan, Sir" => 28398, "Baum, L. Frank (Lyman Frank)" => 23663, "Twain, Mark" => 48995, "Miles, Alexander" => 21775 }

78

Copyright © John Leach 2008. All rights reserved.


puts "Hash natural ordering" authors.each { |name, count| puts " #{name} (#{count})" }

Here we define the list, authors, deliberately in no particular order, using a Hash object, followed by the standard Hash.each iterator. The results look something like: C:\Courses\LUGPC8\Review>code_blocks.rb Hash natural ordering Dickens, Charles (37563) Miles, Alexander (21775) Twain, Mark (48995) Austen, Jane (39508) Verne, Jules (24615) Shakespeare, William (31758) Tzu, Sun (22447) Baum, L. Frank (Lyman Frank) (23663) Doyle, Arthur Conan, Sir (28398) Thomson, J. Arthur (36825)

On my computer, the Hash's natural order does not correspond to the order defined in the code, nor to an ordered list by author. What we actually want is an iterator which gives us the list by ordered author name. So let's create the iterator: # Singleton method to get authors by ordered name def authors.each_by_author(&block) return unless block self.keys.sort.each { |name| yield name, self[name] } end puts "\nHash ordered by author" authors.each_by_author { |name, count| puts " #{name} (#{count})" }

Just as for JavaScript, Ruby allows us to add a method to a single instance object â&#x20AC;&#x201C; all we have to do is add the instance variable name, authors, to the method definition. We sort the Hash.keys array (the author names), and then use the Array.each iterator and then yield the author name and popularity (the Hash key's value). The result is: Hash ordered by author Austen, Jane (39508) Baum, L. Frank (Lyman Frank) (23663) Dickens, Charles (37563) Doyle, Arthur Conan, Sir (28398) Miles, Alexander (21775) Shakespeare, William (31758) Thomson, J. Arthur (36825) Twain, Mark (48995) Tzu, Sun (22447) Verne, Jules (24615)

Modules - One Solution to Two Problems

Up to now we've used a few simple names for our classes. Eventually, as our programs become more complex, there is a risk that we want to use the same name for two completely different classes. Unfortunately, Ruby doesn't allow this. If you think about the Ruby libraries, you can probably imagine that two different authors could easily write the same Result or Element class, for example. A second problem is that Ruby only allows single class inheritance. There will be times when you'd really like to inherit functionality from more than one class, but Ruby won't let you. You're stuck with choosing the most useful super class, and Copyright Š John Leach 2008. All rights reserved.

79


rewriting the functionality of the other classes. This leads to duplication of code, which is not a good idea at all. Doing things once, in one place, is the whole point of writing object oriented code in the first place. Ruby's solution to both problems is the module. You define a module inside a module ... end code block. A module can be used as a namespace for one or more classes. This basically means that we can put class names into a container which also has a name. So we can differentiate two classes with the same name, by using two differently named modules. When more than one class requires the same method or methods, and it is difficult to inherit one class from the other, we can define the methods in a module, and then include it in those classes. In Ruby terms, this is known as a mixin â&#x20AC;&#x201C; we mix in the methods to the classes. The following highly contrived code example, modules.rb, shows examples of these techniques: 01 #!/usr/local/bin/ruby -w 02 03 module Mixin 04 Version = "3.0.11" 05 def Mixin.version 06 class_version(Mixin) 07 end 08 def Mixin.class_version(obj) 09 "#{obj} Version #{obj::Version}" 10 end 11 def time 12 @time 13 end 14 end 15

The first module, Mixin defines a constant Version, two module methods Mixin.version and Mixin.class_version, and an instance method time. Well, I did warn you it was contrived. 16 module Search 17 class Result 18 include Mixin 19 Version = "1.1.2" 20 def initialize 21 @time = 0.000212 22 end 23 def self.version 24 "#{Mixin.class_version(self)}, #{Mixin.version}" 25 end 26 end 27 end 28

The second module Search acts as a namespace for the class Result. In addition it includes the Mixin module on line 18, and sets a Version constant on line 19. 29 module Racing 30 class Result 31 include Mixin 32 def initialize(time) 33 @time = time 34 end 35 def self.version 36 "#{Mixin.class_version(self)}, Beta 3" 37 end

80

Copyright Š John Leach 2008. All rights reserved.


38 end 39 end 40

The third module, Racing, acts as a namespace for another class named Result. It also includes the Mixin module. Different from the second module, it does not define its own Version constant. Within either module, Search or Racing, there is only one Result class. Although there are only a couple of methods for these classes, you can probably imagine that they will have very different behaviours. The first will probably have a file_path and word_count, the second might have a team, racing_car and driver. Within each Result class, however, we want a version class method, and they both need to have a time instance method – Search::Result has the time in seconds (with microsecond precision) to perform the search, Racing::Result has the driver's average lap time in seconds (with millisecond precision). Because their behaviour is so different, we wouldn't want to simply inherit one from the other, but we can include the Mixin module to both. By doing that, all the methods defined in Mixin become methods of the class. 41 42 43 44 45 46 47 48 49 50 51 52

puts "Search::Result::Version #{Search::Result::Version}" puts "Racing::Result::Version #{Racing::Result::Version}" puts Search::Result.version puts Racing::Result.version s_r = Search::Result.new r_r = Racing::Result.new(104.123) puts "s_r #{s_r.inspect}" puts "r_r #{r_r.inspect}" puts "s_r.time #{s_r.time}" puts "r_r.time #{r_r.time}" puts "Mixin::Version #{Mixin::Version}" puts "Mixin.time #{Mixin.time}"

To wrap up we write a series of puts to see the results of our mixins. These print out the two Result classes' Version constants, the version class method values, create two instances of the two Result classes, inspect them, get their time method values, and finally output the Mixin modules Version constant and time method value. The resulting output looks like: 01 C:\Courses\LUGPC8\Review>modules.rb 02 Search::Result::Version 1.1.2 03 Racing::Result::Version 3.0.11 04 Search::Result Version 1.1.2, Mixin Version 3.0.11 05 Racing::Result Version 3.0.11, Beta 3 06 s_r #<Search::Result:0x2ae4870 @time=0.000212> 07 r_r #<Racing::Result:0x2ae4028 @time=104.123> 08 s_r.time 0.000212 09 r_r.time 104.123 10 Mixin::Version 3.0.11 11 C:/Courses/LUGPC8/Review/modules.rb:52: undefined method `time' for Mixin:Module (NoMethodError)

Lines 41 to 44 give the output on lines 2 to 5. You might be surprised that Search::Result can change the Mixin::Version constant value. In effect Ruby does not change the value, it changes the reference. When you include a mixin, the constants and methods are not copied into the class, they are added to the list of the class' references – see the Module documentation for methods that list Copyright © John Leach 2008. All rights reserved.

81


class_variables, constants, included_modules,

instance_methods

of a

Lines 47 to 50 give the output on lines 6 to 9. Both classes use the mixin method but using their own @time instance variable values.

time

class.

and

Finally, lines 54 to 55 give the output on lines 10 to 11. Although we can print the Mixin::Version constant value, and we could call the module method Mixin.version if we wanted to, we can't do the same with the time method, since a module itself cannot have instance methods. Ruby uses mixins to define the Object class. Some of the methods come from Object, and some from the modules Kernel and Module. This way, we don't end up with a huge amount of code in the Object class, and the module names also help differentiate the behaviour of the methods themselves. Two other notable mixin modules are Comparable, and Enumerable. For any given class, once the comparison method (<=>) has been defined, the Comparable mixin gives us six other methods (<, <=, ==, >=, >, and between?) for free. Similarly with Enumerable, once we have defined an each method, this mixin can provide a dozen other iterator methods. Unit Testing

Ruby provides a unit testing library which makes testing our code trivial. We didn't have time last week, so I've added a unit_test.rb file which tests our SearchEngine from the 'Ruby and Ajax' lesson, together with the search.rb and word_count.rb files, and two simple text files which we'll test against. 01 02 03 04 05 06 07 08 09

#!/usr/local/bin/ruby -w require 'search' require 'test/unit' class TestSearch < Test::Unit::TestCase def setup @engine ||= SearchEngine.new('./text', '.', 'http://www.jhl.it') end

We create a class (which begins with Test) and inherits from the unit test library. Before the test methods are run, we can prepare the data we'll be testing in the setup method. Where this requires using resources, we can release those resources after each test method has run by defining a teardown method, though this wasn't needed in our example. Note that setup is called before each test method, and teardown after each test method. We don't need to parse the files for each test, so I use the short circuit on line 8 to ensure that @engine is created only if it isn't nil. See the documentation for for more information. 10 11 12 13 14

82

Test::Unit::TestCase,

and

Test::Unit::Assertions

def test_files assert_equal(2, @engine.files.length) @engine.files.each do |file| assert_equal("http://www.jhl.it/text", file.url[0...22]) case file.file

Copyright Š John Leach 2008. All rights reserved.


15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

when './text/one_word.txt' assert_equal('frabjous', file.title) assert_equal(1, file.count) assert_equal(1, file.unique_count) assert_equal(1, file.word_count('frabjous')) assert_equal(0, file.word_count('inexistant')) when './text/four_words.txt' assert_equal('O frabjous day! Callooh! Callay!', file.title) assert_equal(10, file.count) assert_equal(4, file.unique_count) assert_equal(0, file.word_count('O')) assert_equal(4, file.word_count('frabjous')) assert_equal(3, file.word_count('day')) assert_equal(2, file.word_count('Callooh')) assert_equal(1, file.word_count('Callay')) else flunk "Unexpected #{file.file}" end end end

The test_files method checks that both files have been parsed correctly. It also checks that no additional files have been added, as this will probably invalidate the results of other tests. 35 def test_results 36 results = @engine.search('inexistent') 37 assert_equal([], results) 38 results = @engine.search('day') 39 assert_equal(1, results.length) 40 result = results[0] 41 assert_equal('http://www.jhl.it/text/four_words.txt', result.url) 42 assert_equal('O frabjous day! Callooh! Callay!', result.title) 43 assert_equal(3, result.count) 44 results = @engine.search('frabjous') 45 assert_equal(2, results.length) 46 result = results[0] 47 assert_equal('http://www.jhl.it/text/four_words.txt', result.url) 48 assert_equal('O frabjous day! Callooh! Callay!', result.title) 49 assert_equal(4, result.count) 50 result = results[1] 51 assert_equal('http://www.jhl.it/text/one_word.txt', result.url) 52 assert_equal('frabjous', result.title) 53 assert_equal(1, result.count) 54 end 55 end

The second test runs the SearchEngine through its paces. We check that the results array is valid for the case of zero, one or two results (lines 36, 38, and 44). The output of running this little program is as follows: C:\Courses\LUGPC8\Review>unit_test.rb Loaded suite C:/Courses/LUGPC8/Review/unit_test Started .. Finished in 0.015 seconds. 2 tests, 28 assertions, 0 failures, 0 errors

Overview of Ruby on Rails is a framework for developing web applications in the Ruby language. The code that we develop is usually very intimately tied to the framework. This makes frameworks quite different from libraries, where code can usually be organised to remove such close dependencies, allowing a program to switch from one library to Ruby on Rails

Copyright Š John Leach 2008. All rights reserved.

83


another. Choosing the framework for a web application is therefore a major and permanent decision. It will be difficult if not impossible to switch frameworks once a significant part of the application has been developed. In the Ruby world, however, the choice is relatively simple, because Ruby on Rails is by far the dominant web application framework. It has achieved this dominance by reducing the complexity of web applications using a technique now known as convention over configuration. What's in the Box?

Rails comprises six ruby gems, which we'll look at briefly, and a set of code generators and other scripts. – the rails gem provides the core “glue” code which binds together the other five gems. As Ruby on Rails programmers we won't have many dealings with this gem, it does its work and keeps out of the way.

Rails

ActiveSupport

ActiveRecord

ActiveResource

ActionPack

ActionMailer

– this gem provides a host of classes and methods, all designed to make our job easier. It adds methods to Ruby classes, provides helpers for enumerations, and adds Unicode support to strings. – provides an object relational mapper between the database tables and records, and the data objects in our web application. This gem provides near transparent methods to extract, modify and permanently save the data that we'll be using. – a complete web services kit, strongly leaning towards RESTful implementations. We won't have time to look at this particular gem. – comprises ActionController and ActionView. The former provides a framework to analyse and elaborate a user's request, the latter provides templating engines, and helper classes for displaying the response to the user. We'll look at these in more detail a little later on. – provides a suite of classes and methods for email services. Again, we won't have time to use this gem in our web application.

The Rails gem also performs additional tasks via a series of scripts, both to maintain the conventions it advocates, and to provide scaffolding code upon which we can derive our own work. The first and fundamental script is rails, which creates the basic application. The application consists of the following folders: •

84

app/ - the application and views/.

code, in particular the

controllers/, helpers/, models/

- configuration files, and database connections.

config/

db/

doc/

- generated application documentation.

lib/

- shared (library) code.

- the database and migration files.

Copyright © John Leach 2008. All rights reserved.


- server logs.

log/

public/

- public static files.

script/

- utility and code generation scripts.

test/

tmp/

vendor/

- unit, functional and integration tests, fixtures and mock classes.

- temporary files created during the execution of the web application, such as cached pages, sockets and sessions. - the rails code and any additional plugin code.

That may seem like a lot of folders, but each has its own distinct purpose. During the next two lessons, we'll be doing work in the app/, db/, and public/ folders, and make brief visits to the config/ folder. The log/ and tmp/ folders are used by the web server itself, though the log files in particular can be a useful debugging tool. Our application won't make use of the doc/, lib/, and vendor/ folders, as such. Due to a lack of time, we will also be ignoring the test/ folder. Rails provides a sophisticated testing structure however, which I urge you to take a look at, from unit testing, right through to integration testing. Models, Views and Controllers

The model-view-controller pattern is used by Rails, and most other web application frameworks, to distribute the work that the code does into separate, distinct areas of concern. The model takes care of the data, the view handles displaying information, and the controller coordinates the whole thing. In Rails, the model consists of one or more classes that define the data that we'll be using in our application. These classes live in the apps/models and db/migrate folders. They make extensive use of the ActiveRecord classes and methods to perform CRUD (create, read, update and delete) operations on the data, via the database tables. The view consists of one or more (usually many more) classes that define the data presentation. These classes live in the apps/views folder. They are for the most part templates, though a great deal of functionality is provided by the ActionView classes. The controller handles a specific aspect of the application. The complete web application usually consists of more than one controller. These classes live in the apps/controllers folder. Again, a great deal of functionality is provided to our controllers via the ActionController classes. So how do these three main components work together? Let's look at an example, and 'talk' through the code. Lets suppose that a user sends a request for the URL /users/show/1. Here is a simplified scheme of what happens: 1. Rails receives the request, and uses config/routes.rb to decide which controller is to be used. It also decides the controller method to call, and any further information that needs to be given to that method. In this case, it maps /users to the UsersController class, found in apps/controllers/users_controller.rb, it maps /show to that controller's Copyright © John Leach 2008. All rights reserved.

85


show

method, and it maps /1 to the parameter identifier, id.

2. At this point Rails will create a UsersController instance, and call the show method, after setting the id to 1. The controller then 'asks' the data model, User, to read the user data whose identifier is 1. Assuming that the user data exists, the data model will return a User object which the controller can then pass along to the template, which by convention will be in apps/views/users/show.html.erb. Of couse if there is no user data with an identifier of 1, the model will give back nil, and the controller can decide to show some other template, probably containing an error message. 3. The template show.html.erb then prepares the response, using HTML, containing some or all of the information found in the User object. 4. The controller's show method terminates, and Rails takes the prepared response, which it sends back to the browser. Each of the three components, model, view and controller, work in collaboration with each other, but without knowing the intimate details of what exactly the others do. This is what is meant by distinct, separate areas of concern. The model and view don't even (need to) know that the controller exists. The model doesn't (need to) know that the view exists. The controller knows the model and view, but doesn't know how they go about their business, it only needs to know what to ask for, in the case of the model, and what to pass along, in the case of the view. Finally the view only knows about the model, and how to extract information from that model, it doesn't know how the model objects were created, or by whom.

Source Files All the source files for this lesson can be found in the distributed under the GNU Lesser General Public License.

LUGPC8.zip

archived file,

What's Next? We'll take our knowledge of Ruby a step further by building our very own Ruby on Rails web application. Lesson 9 will take place on Monday, 10th March 2008.

86

Copyright Š John Leach 2008. All rights reserved.


Ruby on Rails, Part 1 LUG Programming Course, 10th March 2008 This week's lesson starts us on a two part adventure building a web application using Ruby and the Ruby on Rails framework. You'll need to install Ruby on Rails – we're using version 2.0.2. Our application also uses RMagick and SQLite. Installing RMagick is explained in this FAQ, we're using version 2.0.0. Installing SQLite is explained in this How-to, we're using the Ruby SQLite3 wrapper version 1.2.1. Windows users will need both the .exe and .dll versions of SQLite. The Ruby on Rails Wiki page also gives helpful information for SQLite. The lesson went quite well, and although there was a lot to cover, we overran by only 15 minutes.

The WebAlbum Application We'll build a web application for multiple users which allows then to upload images into albums. What? It's already been done? Well I'll admit this isn't going to get Flickr worried, but it will demonstrate many aspects of the Ruby on Rails framework. The application will allow any visitor to register themselves as a user. Then they can create unlimited numbers of albums, each with unlimited numbers of pictures. Users can edit and modify their own albums and pictures. Logged in users see only their own albums and pictures. Anonymous visitors can see all the users, their albums and pictures. The Real World I admit that these specifications are not very realistic. You'd probably also want private albums and pictures, which you'd then allow a few chosen family members and friends to see. We simply won't have the time in two lessons to do that, but you're welcome to extend the application yourself. Reality Check

Have you noticed that we always manage to specify the program in just a couple of paragraphs? You've probably already developed some mental picture of how it's going to work, you can see the list of albums, you're already clicking on them to see the list of thumbnail pictures, choosing one, and then seeing it full screen. All that in just 30 seconds. Unfortunately, we need to convert that mental image into a program, and even Ruby on Rails can't read minds. Those 30 seconds are going to translate into at least 4 hours of work. This is the classic client / developer impedance mismatch. You, the client, are thinking “instant”, whereas we, the developers, are thinking “half a day's work”. The other problem is that the client's mental image may also be very different from the developer's. That can lead to nasty surprises. Copyright © John Leach 2008. All rights reserved.

87


Since this application is going to take more than one lesson to explain, we'll use the “release early and often” open source solution. We won't be able to finish the application this week, but we'll leave the fictitious client something to “play with” at the end of the lesson. It may not look marvellous, but it'll work. Using Rails Generators

Rails comes with a suite of code generators which prepare the way for our own efforts. The first we'll look at is the basic application generator called rails. It creates much of the boilerplate code, including the three 'classic' environments test, development and production, and prepares the database. As always, there is an archived file at the end of this discussion, containing all the code for the lesson. What follows here is an explanation of how we get to produce that code. Do try to build the application yourself, you'll find it helps in the learning process. So, let's get going. Create a folder for the project (mine is /Courses/LUGPC9, but you can choose any name you like). Open a console in that folder and type: rails WebAlbum --database=sqlite3

What you should see in the console is: C:\Courses\LUGPC9>rails WebAlbum -–database=sqlite3 create create app/controllers create app/helpers create app/models create app/views/layouts create config/environments create config/initializers create db create doc create lib create lib/tasks create log create public/images create public/javascripts create public/stylesheets create script/performance create script/process create test/fixtures create test/functional create test/integration create test/mocks/development create test/mocks/test create test/unit create vendor create vendor/plugins create tmp/sessions create tmp/sockets create tmp/cache create tmp/pids create Rakefile create README create app/controllers/application.rb create app/helpers/application_helper.rb create test/test_helper.rb create config/database.yml create config/routes.rb create public/.htaccess create config/initializers/inflections.rb create config/initializers/mime_types.rb

88

Copyright © John Leach 2008. All rights reserved.


create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create create

config/boot.rb config/environment.rb config/environments/production.rb config/environments/development.rb config/environments/test.rb script/about script/console script/destroy script/generate script/performance/benchmarker script/performance/profiler script/performance/request script/process/reaper script/process/spawner script/process/inspector script/runner script/server script/plugin public/dispatch.rb public/dispatch.cgi public/dispatch.fcgi public/404.html public/422.html public/500.html public/index.html public/favicon.ico public/robots.txt public/images/rails.png public/javascripts/prototype.js public/javascripts/effects.js public/javascripts/dragdrop.js public/javascripts/controls.js public/javascripts/application.js doc/README_FOR_APP log/server.log log/production.log log/development.log log/test.log

Keep calm. All those files might look frightening, but for the time being we're only interested in three of the folders; app - where we put our web application code, public – where we put our static files, and db – where the database lives. Now make WebAlbum the current folder in your console. All further console work is going to take place exclusively inside WebAlbum. Although we're using the latest version of Rails, it is a fast moving target. Newer versions may break our code, so we want to keep a local copy of Rails right in our application. We do that with the freeze command: rake rails:freeze:gems

The output will look something like: C:\Courses\LUGPC9\WebAlbum>rake rails:freeze:gems (in C:/Courses/LUGPC9/WebAlbum) Freezing to the gems for Rails 2.0.2 Unpacked gem: 'activesupport-2.0.2' Unpacked gem: 'activerecord-2.0.2' Unpacked gem: 'actionpack-2.0.2' Unpacked gem: 'actionmailer-2.0.2' Unpacked gem: 'activeresource-2.0.2' Unpacked gem: 'rails-2.0.2'

Now our application will use the Rails code (in the vendor folder), and not the Rails code in our Ruby distribution. Next we'll check that the database settings are working. Type: Copyright © John Leach 2008. All rights reserved.

89


rake db:migrate

The output should be: C:\Courses\LUGPC9\WebAlbum>rake db:migrate (in C:/Courses/LUGPC9/WebAlbum)

The db folder will now contain two files: db/ development.sqlite3 schema.rb

So far, so good. The next step is to generate the scaffolding code for our users. We'll start off with just a couple of fields, we'll add additional fields in a moment. Type: ruby script/generate scaffold user name:string motto:string

The output will look something like: C:\Courses\LUGPC9\WebAlbum>ruby script/generate scaffold user name:string motto:string exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/users exists app/views/layouts/ exists test/functional/ exists test/unit/ create app/views/users/index.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/edit.html.erb create app/views/layouts/users.html.erb create public/stylesheets/scaffold.css dependency model exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/user.rb create test/unit/user_test.rb create test/fixtures/users.yml create db/migrate create db/migrate/001_create_users.rb create app/controllers/users_controller.rb create test/functional/users_controller_test.rb create app/helpers/users_helper.rb route map.resources :users

Again, please stay seated, the Captain has everything under control â&#x20AC;&#x201C; I hope. Forget the exists messages and look at the create messages. Rails created a controller app/controllers/users_controller.rb, a model app/models/user.rb, and four views for our user - app/views/users/*.html.erb. The model-view-controller pattern is used by Rails, and most other web application frameworks, to distribute the work. The model takes care of the data, the view handles displaying information, and the controller coordinates the whole thing. Rails also created a database migrate class - db/migrate/001_create_users.rb. This class inherits from ActiveRecord::Migration which provides methods to change the database structure. We need a few more fields than we defined in the scaffold script, so open this file, and modify it to look like: class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :name, :limit => 40, :null => false t.string :motto, :limit => 80 t.string :salted_password, :limit => 40, :null => false

90

Copyright Š John Leach 2008. All rights reserved.


t.string :salt, :limit => 40, :null => false t.timestamps end add_index :users, [:name], :name => :users_name_index, :unique => true end def self.down remove_index :users, :name => :users_name_index drop_table :users end end

I've highlighted the modifications so that you can see the changes from the original file. ActiveRecord::Migration is Rails' way of version controlling the database. We can keep modifying the database, adding new migration files, and Rails will keep track of it all. If we make a mistake, we can usually retrace our steps by reverting to a previous version, and carry on from there. Our CreateUsers class has two class methods (that's why they start with self), up adds the modifications to the database from the previous version, and down reverts the database to the previous version. - it undoes whatever up did, usually in reverse order. Our

method creates the users database table, and a database index for the user name. The down method removes the index and then the table, in that order. Our users can have a name and a motto (something like “I am the greatest”, if you're Mohammed Ali). I'll talk about the salt stuff a little later. up

Right now we're at version 0, so let's update the database. Type: rake db:migrate

The output will be similar to the following: C:\Courses\LUGPC9\WebAlbum>rake db:migrate (in C:/Courses/LUGPC9/WebAlbum) == 1 CreateUsers: migrating =================================================== -- create_table(:users) -> 0.0940s -- add_index("users", [:name], {:name=>:users_name_index, :unique=>true}) -> 0.2500s == 1 CreateUsers: migrated (0.3440s) ==========================================

If you want to check that you can revert to the previous version, then add the version number to the rake command: rake db:migrate VERSION=0

The output will be similar to the following: C:\Courses\LUGPC9\WebAlbum>rake db:migrate VERSION=0 (in C:/Courses/LUGPC9/WebAlbum) == 1 CreateUsers: reverting =================================================== -- remove_index(:users, {:name=>:users_name_index}) -> 0.0620s -- drop_table(:users) -> 0.0780s == 1 CreateUsers: reverted (0.1400s) ==========================================

Remember to run rake again without the version number afterwards. Now let's take a look at what we've got. Start the web application, by typing: ruby script/server

The output will be as follows: Copyright © John Leach 2008. All rights reserved.

91


C:\Courses\LUGPC9\WebAlbum>ruby script/server => Booting WEBrick... => Rails application started on http://0.0.0.0:3000 => Ctrl-C to shutdown server; call with --help for options [2008-02-22 10:11:21] INFO WEBrick 1.3.1 [2008-02-22 10:11:21] INFO ruby 1.8.6 (2007-09-24) [i386-mswin32] [2008-02-22 10:11:21] INFO WEBrick::HTTPServer#start: pid=3124 port=3000

Start up your favourite browser, and type http://localhost:3000/ in the location bar. When the Rails home page appears, click on the “About your application's environment” link, see below.

Now lets look at what Rails prepared for us as our location, and load the page.

users

views. Add

users

to the

Not too exciting, because we haven't actually got any users yet, however Rails provides a lot of boilerplate code for us. The next step will be to allow us to create a new user. What? You want to click on the “New user” link? Alright, but don't try to create a new user - we aren't quite ready for that.

Now shut down the server (Ctrl+C), we've got some more coding to do.

92

Copyright © John Leach 2008. All rights reserved.


Our First User

Each of our users will have a password. We don't want to keep that password as plain text, because, well, someone might steal it. I'm not talking about stealing a single user's password, I'm talking about the whole database. The 'salt and shaker' process used here is probably a good enough solution to a potentially big problem. Ranting aside, let's look at the code. This is the modified app/models/user.rb file: require 'digest/sha1' class User < ActiveRecord::Base attr_readonly :name attr_reader :password attr_accessor :password_confirmation validates_presence_of :name validates_length_of :name, :within => 1..40 validates_uniqueness_of :name validates_length_of

:motto, :within => 0..80

validates_confirmation_of :password def password=(pword) @password = pword.strip return if @password.length < 6 || @password.length > 40 self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{rand.to_s}--") self.salted_password = User.encrypted_password(self.salt, @password) end def validate_on_create if @password.length < 6 || @password.length > 40 errors.add(:password, "must have between 6 and 40 characters") end end def self.authenticate(name, password) user = find(:first, :conditions => [ "name = ?", name ]) if user if user.salted_password != encrypted_password(user.salt, password) user = nil end end user end private def self.encrypted_password(salt, password) Digest::SHA1.hexdigest("--#{salt}--#{password}--") end end

ActiveRecord::Base is just ActiveRecord::Validations. ActiveRecord::Errors class,

bristling with methods, including those found in Errors are handled by an instance of the called errors. We use attr_readonly to make the name field a constant, because we don't want the user changing his user name. The

validates_* methods will do all the checking for us, including confirming that password is the same as password_confirmation (we'll ask for the password twice

to ensure that the user doesn't make a typing mishtake). In the form we'll ask for a field called password, which we'll actually store in the database field salted_password, using the password= method. The validate_on_create method makes sure that a password is given when we attempt to create a new user. The class Copyright Š John Leach 2008. All rights reserved.

93


method authenticate is used when the user attempts to log in to our application. The private class method encrypted_password is used to calculate the, uhm, encrypted password. Now we'll need to make some modifications to the ERB template for a new user, in the file app/views/user/new.html.erb: <h1>New user</h1> <%= error_messages_for :user %> <% form_for(@user) do |f| %> <div> <%= f.label :name, 'Name:' %> <%= f.text_field :name, :size =>40, :maxlength => 40 %> </div> <div> <%= f.label :motto, 'Motto:' %> <%= f.text_field :motto, :size => 40, :maxlength => 80 %> </div> <div> <%= f.label :password, 'Password:' %> <%= f.password_field :password, :size => 40, :maxlength => 40 %> </div> <div> <%= f.label :password_confirmation, 'Confirm password:' %> <%= f.password_field :password_confirmation, :size => 40, :maxlength => 40 %> </div> <div> <%= f.submit "Create" %> </div> <% end %> <%= link_to 'Back', users_path %>

Here we've just added the two fields for password, and password_confirmation, and added a few attributes. We've also used <div> elements instead of <p>, and replaced the <b>...</b> elements with f.label helper method calls. Now we're ready to add a user. Fire up the server again: ruby script/server

Open the browser and go to the location should now see something like this:

http://localhost:3000/users/new.

You

Now I know you're just dying to create a user, but try submitting the empty form first. You should see something like this: 94

Copyright Š John Leach 2008. All rights reserved.


Seems like most of the validation is working. Hmm, pity that Rails modified our formatting though. Now fill in the name, the motto (if you want to), the password, but not the password_confirmation:

Well, confirmation validation works as well. At this point, let's really create the user (add the password in the confirm password field):

Now ain't that grand? We've created a user, and we also get a nice green message telling us so. Now we can go back to the index page and see how that looks: Copyright Š John Leach 2008. All rights reserved.

95


What if we want to edit the user properties? That page now looks like:

Hmm, there's work to be done here as well. Certainly the Rails generators have done much of the groundwork for us, but there is still much that can be done to improve things. Scaffolding code should be considered just that - an initial platform on which we construct our own application. The app/views/users/show.html.erb template needs modifying so that we can have better CSS control over the positioning of the elements: <h1>Showing user</h1> <div class='show'> <p class='label'>Name:</p> <p class='value'><%=h @user.name %></p> </div> <div class='show'> <p class='label'>Motto:</p> <p class='value'><%=h @user.motto %></p> </div> <div class='show'> <% if same_user? @user %> <%= link_to 'Edit', edit_user_path(@user) %> | <% end %> <%= link_to 'Back', users_path %> </div>

As a security measure, we've wrapped the link to the edit page with a test that the logged in user is the user being displayed, same_user?. This is explained in a moment. The same applies for the app/views/users/edit.html.erb template, which needs similar modifications to those we made for the new user form: <h1>Editing user</h1> <% if same_user? @user %> <%= error_messages_for :user %> <% form_for(@user) do |f| %> <div> <%= f.label :name, 'Name:' %>

96

Copyright Š John Leach 2008. All rights reserved.


<%= f.text_field :name, :size => 40, :maxlength => 40, :disabled => 'disabled', :class => 'disabled' %> </div> <div> <%= f.label :motto, 'Motto:' %> <%= f.text_field :motto, :size => 40, :maxlength => 80 %> </div> <div> <%= f.submit "Update" %> </div> <% end %> <% else %> <p class='advise'> You must be logged in to change this information. </p> <% end %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %>

This time we skip the entire form if it's not the same_user?, and display an advisory message. Although we display the user name as a text_field, we've also set the disabled flag, so that it can't be modified. In the user list page, app/views/users/index.html.erb, the New user link is at the bottom of the page. As the user list grows, we'll have to scroll down to the bottom to create a new user. We can remedy this by repeating the link at the top of the page: <h1>Listing users</h1> <%= link_to 'New user', new_user_path %> <p>&#160;</p> <table> <tr> <th>Name</th> <th>Motto</th> </tr> <% for user in @users %> <tr> <td><%=h user.name %></td> <td><%=h user.motto %></td> <td><%= link_to 'Show', user %></td> <% if same_user? user %> <td><%= link_to 'Edit', edit_user_path(user) %></td> <% end %> </tr> <% end %> </table> <p>&#160;</p> <%= link_to 'New user', new_user_path %>

We also removed the Destroy link, and wrapped the Edit link as for previous pages. Now shut down the server again (Ctrl+C). You might be interested in seeing what's inside the database, so at the console, type: sqlite3 db/development.sqlite3

Copyright Š John Leach 2008. All rights reserved.

97


You should see something like: C:\Courses\LUGPC9\WebAlbum>sqlite3 db/development.sqlite3 SQLite version 3.5.6 Enter “.help” for instructions sqlite>

First, let's see what tables we have in the database: sqlite> .tables

which gives: schema_info

users

The schema_info table is created by Rails. It contains the current migration version number. The second table contains our users, all one of them. Let's have a look at the table contents, type: sqlite> .header ON sqlite> select * from schema_info;

which gives: version 1

The select... line is an SQL query – actually probably one of the simplest there is. It translates to “select and get all the columns in the schema_info table for all the rows”. Now let's look at the users table: sqlite> select * from users;

which gives: id|name|motto|salted_password|salt|created_at|updated_at 1|jhl|I'm part of the Villafranca LUG team| 56352147b463894ee939eb92c5842f5cbc223a9f|df6480b74d7814c2a51375883f544ef7044d3bd4| 2008-02-29 18:39:05|2008-02-29 18:39:05

I tidied the last output up a bit because of the 80 column line wrapping. I'm not too worried that you'll be able to understand my password. Now type: sqlite> .exit

to leave the SQLite console. You can achieve the same results using the Rails console. Type ruby script/console

Then when the console prompt appears, type like:

User.find :all,

the results will look

C:\Courses\LUGPC9\WebAlbum>ruby script/console Loading development environment (Rails 2.0.2) >> User.find :all => [#<User id: 1, name: "jhl", motto: "I'm part of the Villafranca LUG team", sa lted_password: "56352147b463894ee939eb92c5842f5cbc223a9f", salt: "df6480b74d7814 c2a51375883f544ef7044d3bd4", created_at: "2008-02-22 18:39:05", updated_at: "200 8-02-22 18:39:05">] >> quit

Remember to type quit to exit the console.

98

Copyright © John Leach 2008. All rights reserved.


Pause for Thought Before we can consider the application ready for an 'early' release, we still have the following tasks to complete: • •

We need to make the pages look better. The error highlighting in the form changes the layout of the form itself, which needs correcting.

The user must be able to log in and out.

Only the specific user can edit their own information.

You've probably noticed by now that the templates we've looked at are actually only a fragment of the entire page. In app/views/layouts you'll find a template called users.html.erb. It contains the body of the HTML page, and a yield statement, which passes control to one of our templates. Rails makes great use of such 'partial' templates to reduce the amount of code, and avoid repeated code – which would be worse. We can use this knowledge to make our own 'application wide' layout, complete with header and footer, which well see in a moment. We'd like to change the Rails standard behaviour when highlighting fields with errors, because it currently breaks the layout of our forms. Rails wraps a <div class='fieldWithErrors'> around the offending <label> and <input> elements – I checked using Firebug in Firefox. How do we find the code that does this work? Well, I didn't know either, so I did a file search for the class name fieldWithErrors, which gave back this code snippet from vendor/rails/actionpack/lib/action_view/helpers/active_record_helper.r b: module ActionView class Base @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" } cattr_accessor :field_error_proc end ...

Hmm, so if

we give a different Proc object to the class variable ActionView::Base.field_error_proc then we can change the behaviour. Good. Since we can now create users, we need to be able to log them in and out. As you may already know, HTTP is a stateless protocol, which means that once the server has responded to a browser request, it forgets the whole deal. But now we need to keep the information about the logged in user between multiple requests and responses. Well, you'll be glad to know that it can be done – using cookies on the browser and sessions on the server. All clever stuff which Rails handles transparently for us. Here is the cookie for our little application:

Copyright © John Leach 2008. All rights reserved.

99


It uses the application name, and an encrypted value, which Rails will associate on the server side with the session object. So all we need to do is store the user (or better, the user's identifier) in the session object. Now that we've got the groundwork out of the way, we can get back to coding. The Master Layout Template

As a starting point for improving the web pages, we'll create a master page template. In app/views/layouts rename user.html.erb to web_album.html.erb. Now we'll modify our layout template: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <% title = 'WebAlbum: ' + controller.controller_name.capitalize + ': ' + controller.action_name.capitalize %> <title><%= title %></title> <%= stylesheet_link_tag 'web_album' %> </head> <body> <%= render 'layouts/partials/header' %> <div class='content'> <p style="color: green"><%= flash[:notice] %></p> <%= yield %> </div> <%= render 'layouts/partials/footer' %> </body> </html>

I've highlighted the changes. We calculate the title, using Rails methods, based on the controller name and action. We use the render method to, uhm, render the two partial templates, header and footer, which we'll look at next. In app/views/layouts we'll create a templates. Here is header.html.erb:

/partials

folder for our header and footer

<div class='header'> <div class='title'>WebAlbum</div> <div class='login_form'> <% logged_user = current_user form_tag "/#{controller.controller_name}/#{logged_user ? 'logout' : 'login'}" do %>

100

Copyright Š John Leach 2008. All rights reserved.


<p class='login'> <% if logged_user %> User: <span class='user'><%= link_to logged_user.name, logged_user %></span> <%= submit_tag 'Log out' %> <% else %> Name: <%= text_field_tag :name, params[:name], :size => 15, :maxlength => 40 %> Password: <%= password_field_tag :password, params[:password], :size => 15, :maxlength => 40 %> <%= hidden_field_tag :url, url_for %> <%= submit_tag 'Log in' %> <% end %> </p> <p class='login_notice'><%= flash[:login_notice] %></p> <% end %> </div> </div>

The lion's share of the code here handles the log in (and out) form. It uses the current_user method, explained in a moment, to check if a user is already logged in. If so, the user's name (linked to their user edit page) and the log out button is shown. If not, then the usual name/password form is shown. This form is available on every page, so we use the controller's name, and two actions; login and logout. This is the brief

footer.html.erb:

<div class='footer'> <p>WebAlbum. Brought to you by the Villafranca LUG team.</p> </div>

We've changed the CSS file name in the template, so we'll have to do the same in public/stylesheets. Rename scaffold.css to web_album.css. The modified CSS file contents can be found in the downloadable archive as it is a little too large to list here. Controllers and Helpers

We still have a few minor modifications to make to our Ruby code, before we've completed the tasks for this lesson. We need to write the login and logout action methods, we need to tell the users_controller.rb which layout template to use, and we need to change the error procedure we talked about before. Since all our controllers inherit from ApplicationController in app/controllers/ application.rb, this is were we'll put our action methods: # Filters added to this controller apply to all controllers in the application. # Likewise, all the methods added will be available for all controllers. class ApplicationController < ActionController::Base helper :all # include all helpers, all the time # See ActionController::RequestForgeryProtection for details # Uncomment the :secret if you're not using the cookie session store protect_from_forgery # :secret => 'de30b92a0dfd2a772c000dee4ea91d09' def login session[:user_id] = nil if request.post? user = User.authenticate(params[:name], params[:password]) if user session[:user_id] = user.id else flash[:login_notice] = "Invalid name/password combination" end

Copyright Š John Leach 2008. All rights reserved.

101


redirect_to params[:url] else redirect_to '/users' end end def logout session[:user_id] = nil redirect_to '/users' end helper_method :find_user def find_user(id) User.find(:first, :conditions => [ "id = ?", id ]) end helper_method :current_user def current_user @current_user ||= (session[:user_id] ? find_user(session[:user_id]) : nil) end helper_method :same_user? def same_user?(user) logged_user = current_user return true if logged_user && logged_user.id == user.id false end end

When a user attempts to log in, we check the authenticity, and if successful set the session user_id. If unsuccessful, we show an error message, and reload the current page (which was stored in the form as a hidden field). The worst case situation is if no form was posted at all, in this case we redirect to the users list page. We've also put the current_user and same_user methods here, and marked them both as a helper_method for the view templates, rather than being an action method. In app/controllers/users_controller.rb we'll add layout 'web_album' to the start of the class definition, so that it uses our master template, instead of the default. The file now begins like this: class UsersController < ApplicationController layout 'web_album' ...

One further small modification to this file is to automatically login a newly created user. This means adding a single line of code to the create method: def create @user = User.new(params[:user]) respond_to do |format| if @user.save session[:user_id] = @user.id flash[:notice] = 'User was successfully created.' format.html { redirect_to(@user) } format.xml { render :xml => @user, :status => :created, :location => @user } else format.html { render :action => "new" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end

We want to change the error procedure for all our templates. Rails has a concept of helpers, which are loaded into all of the templates, so this is obviously the place to 102

Copyright Š John Leach 2008. All rights reserved.


put our code. We also uses a same_user method to protect certain links and forms, this can also be placed in the view helpers file. So in app/helpers/application_helper.rb, we'll add the following lines: # Methods added to this helper will be available to all templates in the application. module ApplicationHelper ::ActionView::Base.field_error_proc = Proc.new { |html_tag, instance| "<span class='fieldWithErrors'>#{html_tag}</span>" } end

Because we're inside a module, we have to reference ActionView by preceding it with the scope expression (::).

First Release Now we're ready for our first release. Start up the server again: ruby script/server

Open the browser and go to the location should now see something like this:

http://localhost:3000/users.

You

Clicking on Log in, without specifying the name and password, causes the error message to be displayed. Next we'll look at the show page (click on Show):

Now let's see what the edit page looks like. Log in as villafranca, then click on Edit:

Copyright Š John Leach 2008. All rights reserved.

jhl

with password

103


Next we'll check out the new user page, (click Log

out,

then New

user):

Let's see what the error highlighting looks like now (click on Create):

Finally, creating a new user, also automatically logs in that user:

104

Copyright Š John Leach 2008. All rights reserved.


We seem to have resolved all the problems. That's as far as we're going in this lesson, so we'll give release 0.1 to the fictitious client, while we carry on coding – next week.

Source Files All the source files for this lesson can be found in the LUGPC9.zip archived file. Be warned, though – this is a 2 MB file. The password for both users (jhl and lug) is villafranca. The source code is licensed identically to Ruby on Rails, using the MIT license.

What's Next? We'll complete our Ruby on Rails web application, by adding albums and pictures. Meanwhile, why not try out the current version of the application for yourself ?

Addendum Wrapping the edit.html.erb template using the same_user? method is not by any means the best way to provide security in Rails. At the time I thought it would have taken too long to explain the intricacies of the filtering mechanism. I have since taken the WebAlbum application one step forward in an article I wrote on my company site, which provides such security. Feel free to take a look.

Copyright © John Leach 2008. All rights reserved.

105


Ruby on Rails, Part 2 LUG Programming Course, 17th March 2008 This week's lesson concludes the two part adventure building a web application using Ruby and the Ruby on Rails framework. Although we'll have a working application by the end of the lesson, good enough to play with at home, there will still be some work required to bring it to production environment standards. The lesson went quite well, and although there was a lot to cover, we overran by only 15 minutes. We finished off the evening to celebrate the end of the course with a drink in a local bar.

Creating the Albums and Pictures The next step will be to create another two classes that make up our data model, namely albums and pictures. But, if you remember, we have to do quite a lot of work modifying the migration, model, and views that were generated. Will we have to do all those modifications again? Well, the answer is - not entirely. I'm not too concerned about modifying the migration and model, after all there will only be four files, but we did do a lot of repetitive work on the views, and there'll be eight of them. Now since Rails is an open source project, and we've frozen it in our application, why not modify the templates that generate the code first? So we'll modify four files instead of eight â&#x20AC;&#x201C; but we'll also be modifying less code. If we were going to create a dozen new classes, I'm sure you'd see the advantage. I won't show you the template code here (you'll be seeing enough code as it is), but you'll find the modified template files view_*.html.erb in the folder /vendor/rails/railties/lib/rails_generator/generators/components/scaff old/templates.

Now, in the console type: ruby script/generate scaffold album caption:string description:text user:references

Then modify db/migrate/002_create_albums.rb: class CreateAlbums < ActiveRecord::Migration def self.up create_table :albums do |t| t.string :caption, :limit => 40, :null => false t.text :description t.references :user, :null => false t.integer :pictures_count, :default => 0 t.timestamps end add_index :albums, [:user_id], :name => :albums_user_index end def self.down remove_index :albums, :name => :albums_user_index drop_table :albums end end

So albums will have a (required) caption, an optional, and possibly very lengthy description, and a reference to the user. The pictures_count will be explained in a moment, when we modify the user migration 106

Copyright Š John Leach 2008. All rights reserved.


Now type (all on one line): ruby script/generate scaffold picture caption:string description:text user:references album:references content_type:string width:integer height:integer

Then modify db/migrate/003_create_pictures.rb: class CreatePictures < ActiveRecord::Migration def self.up create_table :pictures do |t| t.string :caption, :limit => 40 t.text :description t.references :user, :null => false t.references :album, :null => false t.string :content_type, :null => false t.integer :width, :null => false t.integer :height, :null => false t.timestamps end add_index :pictures, [:user_id], :name => :pictures_user_index add_index :pictures, [:album_id], :name => :pictures_album_index end def self.down remove_index :pictures, :name => :pictures_album_index remove_index :pictures, :name => :pictures_user_index drop_table :pictures end end

Pictures will have an optional caption, an optional and possibly lengthy description, references to the user and album, and information about the uploaded image – the content_type, width and height. Is that everything? Not quite. Our users are (hopefully) going to end up with many albums and many pictures. How many? Well, we could always run an SQL query on the two tables, but it will be more efficient to keep the count in the user table. In fact, this is one of Rails' specialities – using a simple convention, as you'd expect. We already did this in the albums migration, now you know why. So we'll modify the db/ migrate/001_create_users.rb file too: class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :name, :limit => 40, :null => false t.string :motto, :limit => 80 t.string :salted_password, :limit => 40, :null => false t.string :salt, :limit => 40, :null => false t.integer :albums_count, :default => 0 t.integer :pictures_count, :default => 0 t.timestamps end add_index :users, [:name], :name => :users_name_index, :unique => true end def self.down remove_index :users, :name => :users_name_index drop_table :users end end

However, we'll have to revert the database (and we'll loose the user data too – but there were only two users), and then update again. Type: rake db:migrate VERSION=0 rake db:migrate

Copyright © John Leach 2008. All rights reserved.

107


Which produces the results: C:\Courses\LUGPC10\WebAlbum>rake db:migrate VERSION=0 (in C:/Courses/LUGPC10/WebAlbum) == 1 CreateUsers: reverting =================================================== -- remove_index(:users, {:name=>:users_name_index}) -> 0.0620s -- drop_table(:users) -> 0.0630s == 1 CreateUsers: reverted (0.1250s) ========================================== C:\Courses\LUGPC10\WebAlbum>rake db:migrate (in C:/Courses/LUGPC10/WebAlbum) == 1 CreateUsers: migrating =================================================== -- create_table(:users) -> 0.1570s -- add_index(:users, [:name], {:name=>:users_name_index, :unique=>true}) -> 0.1250s == 1 CreateUsers: migrated (0.2820s) ========================================== == 2 CreateAlbums: migrating ================================================== -- create_table(:albums) -> 0.0940s -- add_index(:albums, [:user_id], {:name=>:albums_user_index, :unique=>false}) -> 0.0780s == 2 CreateAlbums: migrated (0.1720s) ========================================= == 3 CreatePictures: migrating ================================================ -- create_table(:pictures) -> 0.0780s -- add_index(:pictures, [:user_id], {:name=>:pictures_user_index, :unique=>false}) -> 0.0780s -- add_index(:pictures, [:album_id], {:name=>:pictures_album_index, :unique=>false}) -> 0.0780s == 3 CreatePictures: migrated (0.2340s) =======================================

In the previous lesson, we started to get to know the Rails framework as we created the code for our users. We'll continue using this knowledge, and expand our understanding of Rails, as we first write the code for albums, and then for pictures. The Album Controller

Users have albums, users and albums have pictures. The behaviour of our application changes dramatically depending on whether the visitor is logged in or not. When not logged in, we'll show all the albums and all the pictures. When logged in, we'll show only the albums and pictures belonging to the user. Additionally, we can then allow the user to create albums, and create pictures for an album. We used the session object to keep track of the current user, and we'll do the same for the current album. Let's start with the helper methods we'll require. In app/controllers/application.rb add the following lines at the end of the class: ... def logout session[:user_id] = nil session[:album_id] = nil redirect_to '/users' end ... helper_method :find_album def find_album(id) Album.find(:first, :conditions => [ "id = ?", id ]) end helper_method :current_album

108

Copyright Š John Leach 2008. All rights reserved.


def current_album @current_album ||= (session[:album_id] ? find_album(session[:album_id]) : nil) end helper_method :current_album= def current_album=(id) if current_user session[:album_id] = id else session[:album_id] = nil end end helper_method :all_albums def all_albums if current_user return current_user.albums else return Album.find(:all, :order => "id DESC", :limit => 20 ) end end helper_method :all_pictures def all_pictures if current_user return current_user.pictures else return Picture.find(:all, :order => "id DESC", :limit => 20 ) end end helper_method :user_albums def user_albums if current_user return current_user.albums else return [] end end helper_method :current_user_owns? def current_user_owns?(item) logged_user = current_user return true if logged_user && logged_user.id == item.user_id false end end

We added a line to the logout method to remove the :album_id in the session object. Although we want to track the current album, we only want to do so when the user is logged in. Then we added a current_album helper method which works just like the current_user, and we also add an assignment helper method current_album=. We also added helper methods for all_albums and all_pictures, which will give back different results depending whether the visitor is logged in or not. We added a helper method to get the user_albums for the logged in user. Note that when the visitor is not logged in, we limit the results to the latest 20 albums or pictures. Finally, we added a current_user_owns? helper method to check if the currently logged in user owns the album or picture. Now we'll move on and modify the newly generated controller. Again, as for the UsersController, the modifications are fairly superficial, so I won't show the code here. Copyright Š John Leach 2008. All rights reserved.

109


The Album Views

We need to take some precautions when creating new albums - we must have a user identifier. So the 'new' page need additional protection â&#x20AC;&#x201C; we'll tell the visitor to log on, instead of showing the form. The app/views/albums/new.html.erb file will look like: <h1>New album</h1> <% if current_user %> <%= error_messages_for :album %> <% form_for(@album) do |f| %> <div> <%= f.label :caption, 'Caption:' %> <%= f.text_field :caption, :size => 40, :maxlength => 40 %> </div> <div> <%= f.label :description, 'Description:' %> <%= f.text_area :description, :rows => 4, :cols => 38 %> </div> <div> <%= f.hidden_field :user_id, :value => current_user.id %> <%= f.submit "Create", :class => 'button' %> </div> <% end %> <% else %> <p class='advise'> You must be logged in to create an album.<br /> Why not <%= link_to 'register', new_user_path %> now? </p> <% end %> <%= link_to 'Back', albums_path %>

I've highlighted the differences from the original. Basically we've wrapped the form in a 'user logged in?' test, which shows the form if they're logged in, or tells the visitor what to do if they're not. Next is app/views/albums/edit.html.erb, which uses a similar technique, but checking if the current_user_owns? the @album, so I'll skip the source code. The Album Model

Our migration code created references to the user for an album, and references to the user and album for a picture. Those references basically say 'I belong to ...'. This is known as an association, and is part of the database normalisation process. There are many types of associations; one-to-one, one-to-many, many-to-one, and many-to-many. In our data model one user can have many albums and many pictures, one album can belong to one user and have many pictures, and one picture can belong to one user and belong to one album. We need to update our models so that they understand these associations. We'll add the following lines to app/models/user.rb: 110

Copyright Š John Leach 2008. All rights reserved.


... class User < ActiveRecord::Base has_many :albums, :order => "id DESC", :dependent => :destroy has_many :pictures, :order => "id DESC", :dependent => :destroy ...

One this side of the association for albums and pictures, we also supply the ordering, and the dependency mechanism â&#x20AC;&#x201C; this describes what should happen to the albums and pictures, if the user is destroyed. The app/models/albums.rb file will look like: class Album < ActiveRecord::Base belongs_to :user, :counter_cache => true has_many :pictures, :order => "id DESC", :dependent => :destroy validates_presence_of :user_id validates_presence_of :caption validates_length_of :caption, :within => 1..40 end

As well as the associations, we've also added validation rules. The will make Rails update the albums_count in the User class.

:counter_cache

The First (and Second) Album

That's enough coding for now. Start the server, then go to http://localhost:3000/ albums, click on New album, log in and create a couple of albums. Remember that you'll have to create some users too.

Oops, forgot to log in. So register or log in, then create the album.

Let's Add Some Pictures The next obvious step is to add enough code to allow our little web application add Copyright Š John Leach 2008. All rights reserved.

111


some pictures. Wouldn't be much of a WebAlbum without them, I suppose. First things first, though. I'm getting a little tired of having to change the URL from /users to /albums (and /pictures, next), so we'll fix that right now. Since we're about it, we'll add the additional JavaScript and CSS links, too. Open app/views/layouts/web_album.html.erb, and add the following highlighted lines of code: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <%= javascript_include_tag :defaults %> <%= javascript_include_tag 'lightbox' %> <% title = 'WebAlbum: ' + controller.controller_name.capitalize + ': ' + controller.action_name.capitalize %> <title><%= title %></title> <%= stylesheet_link_tag 'web_album' %> <%= stylesheet_link_tag 'lightbox' %> </head> <body> <%= render 'layouts/partials/header' %> <div class='content'> <div class='navigation'> <%= link_to 'Users', users_path %>&#160;|&#160; <%= link_to 'Albums', albums_path %>&#160;|&#160; <%= link_to 'Pictures', pictures_path %> </div> <p style="color: green"><%= flash[:notice] %></p> <%= yield %> </div> <%= render 'layouts/partials/footer' %> </body> </html>

The javascript_include_tag and Lightbox, which we'll look at later on.

stylesheet_link_tag

additions are for

Ah yes, &#160; is the Unicode character for non breaking space. In HTML you can use &nbsp;, but this is XHTML, and named entities other than &amp;, &lt;, &gt;, &quot;, and &apos; are not known to the XHTML parser. That's better, now we have three links the main list pages.

Users, Albums

and

Pictures,

that take us to

Uploading Image Files and RMagick

Settle down comfortably, we're going to be here for a little while – there's some heavy lifting to do. We'll have to take the binary data of the uploaded image file and store it somewhere. We'll also have to make smaller copies of the image, which we'll use in the other views – and that means getting to grips with RMagick, the Ruby image manipulation library, par excellence. In fact, we'll need to save four images: •

The 'original' image,

The 'large' image, scaled to 500 by 500 pixels,

The 'medium' image, scaled to 250 by 250 pixels,

112

Copyright © John Leach 2008. All rights reserved.


â&#x20AC;˘

The 'small' image, scaled to 100 by 100 pixels.

We'll keep the original image, which people can download via a link. The large image will be used to display the scaled picture 'full screen' in our web application, the medium image will be used for the picture view, and the small image for lists. The sizes were chosen to allow screenshots to be made for this article, with a maximum width of 600 pixels. We'll keep these images in another database table, which we'll call images. Keeping binary image data in a database table may not be the best of solutions, especially if we have thousands or millions of images, but it does have the advantage of keeping all the data in one place. You could use some type of file caching mechanism to reduce the database load â&#x20AC;&#x201C; but that's another story that we won't have time for in this lesson. We'll generate the image model, this time using the model generator script, because we won't need any views or a controller, we'll be reusing the ones we've already got. Type the following all on one line: ruby script/generate model image user:references picture:references size:integer data:binary content_type:string width:integer height:integer

Next we'll modify the image migration file db/migrate/004_create_images.rb: class CreateImages < ActiveRecord::Migration def self.up create_table :images do |t| t.references :user, :null => false t.references :picture, :null => false t.integer :size, :null => false t.binary :data, :null => false t.string :content_type, :null => false t.integer :width, :null => false t.integer :height, :null => false t.timestamps end add_index :images, [:picture_id, :size], :name => :images_index end def self.down remove_index :images, :name => :images_index drop_table :images end end

Now we can update the database again, type: rake db:migrate

The next modification is to the Image model in app/models/image.rb: class Image < ActiveRecord::Base belongs_to :user belongs_to :picture validates_presence_of validates_presence_of validates_presence_of validates_presence_of validates_presence_of validates_presence_of validates_presence_of

:user_id :picture_id :size :data :content_type :width :height

Original = 1 Large = 2

Copyright Š John Leach 2008. All rights reserved.

113


Medium = 3 Small = 4 Widths = [ 0, 0, 500, 250, 100 ] Heights = [ 0, 0, 500, 250, 100 ] Names = [ "", "Original", "Large", "Medium", "Small" ] def filename return Image.filename(picture_id, size) end def self.filename(id, size) if size == Original return "#{id}-#{Names[size]}.jpg" else return "#{id}-#{Names[size]}.gif" end end end

Apart from the usual validation rules, we provide constants for the images sizes, and a filename method which calculates the image filename, based on the picture identifier, size, and stored image format. We do not create an association to the album, since Picture already has this. Next we'll look at the Picture class, as this handles the first part of the image uploading code, app/models/picture.rb: require 'RMagick' class Picture < ActiveRecord::Base belongs_to :user, :counter_cache => true belongs_to :album, :counter_cache => true has_many :images, :order => "size ASC", :dependent => :destroy attr_accessor :operation validates_presence_of :user_id validates_presence_of :album_id validates_length_of :caption, :within => 0..40 NoOp = 0 RotateClockWise90 = 1 RotateAntiClockWise90 = 2 Rotate180 = 3 Flip = 4 Flop = 5 def validate_on_create errors.add_to_base(@picture_error) if @picture_error end def picture_file @picture_filename end def picture_file=(field) extract_picture field end def picture_image @picture_image end private def extract_picture(field) @picture_error = @picture_image = nil

114

Copyright Š John Leach 2008. All rights reserved.


if field.class.method_defined? :original_filename @picture_filename = field.original_filename data = field.read if data.length == 0 @picture_error = "Empty picture file: #{@picture_filename}" else begin @picture_image = org = Magick::Image.from_blob(data).first org.format = "JPG" self.content_type = org.mime_type self.width = org.columns self.height = org.rows rescue Magick::ImageMagickError @picture_image = nil @picture_error = "Unknown picture format: #{@picture_filename}" end end else @picture_error = "No picture filename specified" end end end

Everything

should

be

pretty familiar by now – except that big private extract_picture method. Oh, and the constants – they're for image manipulation operations, which we'll see shortly. The extract_picture method takes care of converting the received binary image information into a Magick::Image class. The first thing it does is to check that we've received a binary image, using the reflection method method_defined?. Since it is a class method, we use the field.class method to get the class. If a binary image was sent, the field object will contain an original_filename method (and a read method), otherwise it won't. This is an example of duck typing. We then read the binary data, and check its length. As long as the length is greater than 0, we'll have some binary data which we can convert into an image. At this point we create a new Magick::Image from the data. However, if the data is not a valid image, RMagick will throw an exception. Exceptions are usually caused by rare or unusual events – such as the hard disk being full. In Ruby, we can capture, and act on, an exception by wrapping the method call in a begin ... rescue ... end code block. If everything goes well, the code from the begin up to, but not including the rescue will be executed, then control will pass to the first statement after the end of the block. If, however, an exception occurs (and in our case, more specifically, a Magick::ImageMagickError exception), then the code between the rescue and end will be executed. There is more RMagick code in the Picture controller, which we'll look at next. The Picture Controller

Users and albums have pictures. We'll make pretty much the same superficial modifications to app/controllers/pictures_controller.rb, so I'll skip most of the code for the controller, however, there is a new action which needs a little explaining: Copyright © John Leach 2008. All rights reserved.

115


... # VIEW /image/:id/:size/:filename.:ext def view id = params[:id] size = params[:size].to_i case size when Image::Original, Image::Large, Image::Medium, Image::Small else size = Image::Small end image = find_image id, size if image headers['Last-Modified'] = image.updated_at.httpdate send_data(image.data, :filename => image.filename, :type => image.content_type, :disposition => 'inline', :status => 200) else render :file => "./public/404.html", :type => 'text/html; charset=utf-8', :status => 404 end end include RmagickHelper end

This action takes care of supplying the image data in binary format. A typical image URL would be /image/7/1/7-Original.jpg. In this case, we use the send_data method to send back the binary data, rather than as XHTML. Picture Controller and RMagick

Now we'll look at that include method call in the controller. We need to add a pretty heavyweight set of helper methods, which can modify the received image and create the smaller images. The sort of modifications that we'll provide are the usual; rotating and flipping the image. Here's the code in app/controllers/rmagick_helper.rb: broken down into sections, with explanations: require 'RMagick' module RmagickHelper private def find_image(pic_id, size) Image.find(:first, :conditions => [ "picture_id = ? AND size = ?", pic_id, size ]) end def find_images(pic_id) Image.find(:all, :order => "size ASC", :conditions => [ "picture_id = ?", pic_id ]) end

The classic finders, the first for a specific image, the second for all (four) images, ordered by size. def save_all(picture) begin Picture.transaction do picture.save! org = picture.picture_image images = create_all picture.id, picture.user_id update_images images, org images.each { |image| image.save! } end rescue return false

116

Copyright Š John Leach 2008. All rights reserved.


end true end

The save_all method has the job of saving the picture, and creating and saving all four images. It is called when a new picture is uploaded. It uses a few helper methods, which we'll see shortly, but more importantly, it also handles a database transaction to store the picture, and the images. To consistently store the information in the database, we have to store one picture and four images. The transaction method guarantees that either all five records are stored, or that none of them are stored. def update_all(picture, params) begin Picture.transaction do picture.update_attributes!(params) operation = picture.operation if operation && operation.to_i != Picture::NoOp images = find_images picture.id org = Magick::Image.from_blob(images[0].data).first case operation.to_i when Picture::RotateClockWise90 then changed = org.rotate(90) when Picture::RotateAntiClockWise90 then changed = org.rotate(-90) when Picture::Rotate180 then changed = org.rotate(180) when Picture::Flip then changed = org.flip when Picture::Flop then changed = org.flop else return true end update_images images, changed images.each { |image| image.save! } picture.width = changed.columns picture.height = changed.rows picture.save! end end rescue return false end true end

The update_all method has to use a database transaction as well, to guarantee consistency of the database. It is called when an existing picture is edited. Inside the case statement we handle the actual image manipulation, using some of the many methods available in RMagick. We need to save the picture again afterwards, because the rotate manipulation method can change the width and height of the image. def dimensions(size) [ Image::Widths[size], Image::Heights[size] ] end def create_all(pic_id, [ Image.new(:user_id Image::Original), Image.new(:user_id Image.new(:user_id Image.new(:user_id end

user_id) => user_id, :picture_id => pic_id, :size => => user_id, :picture_id => pic_id, :size => Image::Large), => user_id, :picture_id => pic_id, :size => Image::Medium), => user_id, :picture_id => pic_id, :size => Image::Small) ]

def update_images(images, original) update_image(images[0], original) large = resize_image(original, dimensions(Image::Large)) update_image(images[1], large) update_image(images[2], resize_image(large, dimensions(Image::Medium), true)) update_image(images[3], resize_image(large, dimensions(Image::Small), true))

Copyright Š John Leach 2008. All rights reserved.

117


end def update_image(image, data) image.content_type = data.mime_type image.width = data.columns image.height = data.rows image.data = data.to_blob end

These are the simple helper methods. The update_images method converts the original image into the three smaller scale images. To reduce the workload (which still takes several seconds for large images) the method first reduces the original to the large (500 x 500 pixel) image, and then takes that image to create the smaller versions at 250 x 250, and 100 x 100 pixels, respectively. def resize_image(data, dims, reposition = false) width = dims[0] height = dims[1] geometry = Magick::Geometry.new(width, height, nil, nil, Magick::GreaterGeometry) data.change_geometry(geometry) do |cols, rows, img| img = img.resize(cols, rows) img.background_color = "transparent" img.format = "GIF" if reposition && (cols < width || rows < height) img = img.extent(width, height, -((width - cols) / 2), -((height - rows) / 2)) end img end end end

The resize_image does the real work of scaling down the image. When reposition is set to true, the reduced image is padded out to make it square. Phew! That's the most complex part of the program out of the way. Now we'll look at the, er, views. The Picture Views

Again we need to take precautions when creating new pictures - we must have a logged in user and that user must have at least one album. So the 'new' pages need protection â&#x20AC;&#x201C; we'll tell the visitor to log on, or create an album, instead of showing the form. The app/views/pictures/new.html.erb file looks like: <h1>New picture</h1> <% if current_user %> <% if @albums.length > 0 %> <%= error_messages_for :picture %> <% form_for(@picture, :html => { :enctype => "multipart/form-data", :onsubmit => "return Lightbox.block('Uploading picture, please wait...');" } ) do |f| %> <div> <%= f.label :caption, 'Caption:' %> <%= f.text_field :caption, :size => 40, :maxlength => 40 %> </div> <div> <%= f.label :description, 'Description:' %>

118

Copyright Š John Leach 2008. All rights reserved.


<%= f.text_area :description, :rows => 4, :cols => 38 %> </div> <div> <%= f.hidden_field :user_id, :value => current_user.id %> <%= f.label :album_id, 'Album:' %> <% current_album_id = current_album ? current_album.id : @albums[0].id options = @albums.collect { |album| [album.caption, album.id] } %> <%= f.select :album_id, options, :selected => current_album_id %> </div> <div> <%= f.label :picture_file, 'Picture file:' %> <%= f.file_field :picture_file %> </div> <div> <%= f.submit "Create", :class = 'button' %> </div> <% end %> <% else %> <p class='advise'> You must have an album to create a picture.<br /> Why not <%= link_to 'create', new_album_path %> one now? </p> <% end %> <% else %> <p class='advise'> You must be logged in to create a picture.<br /> Why not <%= link_to 'register', new_user_path %> now? </p> <% end %> <%= link_to 'Back', pictures_path %>

We have two wrapper tests here, the outer 'logged in?' test, and the inner 'has albums?' test. Each sends back a different advisory message. The more interesting code lies inside the form. Firstly, we have to set the form encoding type attribute (enctype) to 'multipart/form-data', since the form will be posting an image in binary format, together with the other parameters. Secondly, we have added two new form input types; select and file_field. The file_field is pretty straightforward â&#x20AC;&#x201C; it lets the user choose a file (hopefully an image file) to be uploaded to the server. The select input lets the user choose the album to add the picture to. This produces a drop down list, where one of the albums can be chosen. The two lines of code above the select input set two local variables; current_album_id, and options. The current_album_id will be either the session stored current_album identifier, or if there isn't one, the identifier of the first album in the list. The options variable is an array of album captions (what the user will see in the drop down list) and album identifiers (what our code requires to be able to identify the album). In this case we use the collect iterator, which puts the results in an array. The onsubmit attribute uses Lightbox to provide visual feedback while the image is being uploaded, which can take several seconds for a large image file. Next, let's look at the app/views/pictures/edit.html.erb file: Copyright Š John Leach 2008. All rights reserved.

119


<h1>Editing picture</h1> <% if current_user_owns? @picture %> <%= error_messages_for :picture %> <% form_for(@picture, :html => { :onsubmit => "return Lightbox.block('Modifying picture, please wait...');" } ) do |f| %> <div> <label>Image:</label> <span class='gallery'><%= picture_img @picture, Image::Medium %></span> </div> <div> <%= f.label :caption, 'Caption:' %> <%= f.text_field :caption, :size => 40, :maxlength => 40 %> </div> <div> <%= f.label :description, 'Description:' %> <%= f.text_area :description, :rows => 4, :cols => 38 %> </div> <div> <%= f.label :operation, 'Operation:' %> <% options = [ [ "None", Picture::NoOp ], [ "Rotate clockwise 90 degrees", Picture::RotateClockWise90 ], [ "Rotate anti-clockwise 90 degrees", Picture::RotateAntiClockWise90 ], [ "Rotate 180 degrees", Picture::Rotate180 ], [ "Vertical mirror image", Picture::Flip ], [ "Horizontal mirror image", Picture::Flop ] ] %> <%= f.select :operation, options, :selected => Picture::NoOp %> </div> <div> <%= f.hidden_field :user_id, :value => current_user.id %> <%= f.label :album_id, 'Album:' %> <% options = @albums.collect { |album| [album.caption, album.id] } %> <%= f.select :album_id, options, :selected => @picture.album_id %> </div> <div> <%= f.submit "Update", :class => 'button' </div>

%>

<% end %> <% else %> <p class='advise'>You must be logged in as the owner to edit this picture.</p> <% end %> <%= link_to 'Show', @picture %> | <%= link_to 'Back', pictures_path %>

This is very similar to the previous view, except that the :selected value is the current album_id for the picture. We also display the image, and provide an image manipulation selection box, with rotation and flipping options. You'd be surprised how many people send images that then need rotating. Of course, we've also protected the form, by checking that the current_user_owns? the @picture.

120

Copyright Š John Leach 2008. All rights reserved.


Finally, Some Configuration

We've done nothing up to now by way of configuration. We've just followed the conventions, and Rails took care of the rest. However, we need to make two small changes to /config/routes.rb: ActionController::Routing::Routes.draw do |map| map.root :controller => 'users', :action => 'index' map.connect 'image/:id/:size/:filename.:ext', :controller => 'pictures', :action => 'view' ...

Setting map.root (and deleting /public/index.html) defines the controller and action to use for the root URL (/). The map.connect method call specifies the controller and action to send the binary images to the browser.

Adding Some Spice with Lightbox We've done enough coding now to be able to upload and modify our pictures. We can also see them listed in the pictures and album views. We've added links so that we can see the original pictures. Seems that we're pretty much done â&#x20AC;&#x201C; except that we're still missing a little showmanship. Enter Lightbox. Excellence seems to inspire excellence. Developed using Prototype and script.aculo.us, Lightbox provides a simple mechanism together with some pleasant visual effects, to create a 'slide show' for a group of pictures. We'll need to make a couple of small modifications to add Lightbox to our application. First we'll add a couple of helper methods in app/helpers/application_helper.rb, to produce the links: ... def picture_lightbox(picture, size, lightbox_size) caption = picture.caption caption = '...' if caption.nil? || caption.blank? link = h("<a href=\"#{picture_url(picture.id, Image::Original)}\" title=\"View Original\" target=\"_blank\">#{caption.gsub(/'/, '&#39;')}</a>") "<a href='#{picture_url(picture.id, lightbox_size)}' rel='lightbox[list]' title=\"#{link}\">#{picture_img(picture, size)}</a>" end def picture_a(picture, size, html_tag) html_tag = '...' if html_tag.nil? || html_tag.blank? "<a href='#{picture_url(picture.id, size)}' target='_blank'>#{html_tag}</a>" end def picture_img(picture, size) width = 0 height = 0 case size when Image::Original width = picture.width height = picture.height else width = Image::Widths[size] height = Image::Widths[size] end "<img src='#{picture_url(picture.id, size)}' alt=\"#{h picture.caption}\" title=\"#{h picture.caption}\" width='#{width}' height='#{height}' />" end

Copyright Š John Leach 2008. All rights reserved.

121


private def picture_url(id, size) "/image/#{id}/#{size}/#{Image.filename(id, size)}" end end

The picture_lightbox method produces the link which Lightbox uses to do its magic. Since there might not be a caption for the picture, we use ... so that the anchor has some text. The remaining methods produce normal links to the pictures. Finally, (and I mean it this time), we need to activate Lightbox in three views; app/views/albums/show.html.erb, app/views/pictures/index.html.erb, and app/views/pictures/show.html.erb. Just add the following JavaScript code snippet to the start of those pages: <%= javascript_tag "document.observe('dom:loaded', Lightbox.onload);" %>

This ensures that Lightbox is activated when the Document Object Model has been loaded.

The Results For those of you who can't be bothered to download the code, well thanks for reading this far! Here are a few snapshots of the running application, having logged in as jhl with password villafranca. First, the user list page, which is also now the home page:

The albums list page:

122

Copyright Š John Leach 2008. All rights reserved.


The album page, with associated pictures:

Lightbox being used to show a larger version of the small images, the link opens a new window for the original picture:

Copyright Š John Leach 2008. All rights reserved.

123


Source Files All the source files for this lesson can be found in the LUGPC10.zip archived file. Be warned, though â&#x20AC;&#x201C; this is a 4 MB file. The password for both users (jhl and lug) is villafranca. The source code is licensed identically to Ruby on Rails, using the MIT license.

What's Next? Well, this is the end of the programming course, but perhaps not the end of the WebAlbum application. There are still a few small problems which need tidying up. The application still has a 'programmer' look. A few graphics, and better choice of colours would improve things a lot. It's probably a good idea to use the application for a while, you'll soon find where things are a little awkward, perhaps we should add a few links to improve navigation? More importantly, the controllers need protection again accidental or malignant usage via URL construction, such as /users/1/delete. I hope you've enjoyed this programming course. We've been through a lot of topics, and seen a lot of code. Hopefully, the results have justified the effort. Do feel free to use the code for you're own experimentation, the best way to learn is by getting your hands dirty!

124

Copyright Š John Leach 2008. All rights reserved.


Addendum Wrapping the edit.html.erb templates using the current_user_owns? method is not by any means the best way to provide security in Rails. At the time I thought it would have taken too long to explain the intricacies of the filtering mechanism. I have since taken the WebAlbum application one step forward in an article I wrote on my company site, which provides such security. Feel free to take a look.

Copyright Š John Leach 2008. All rights reserved.

125

LUG Programming  

Ruby Programming