TodoApp on Seaside in 218 lines
I have been a fan of Seaside since 2006. I am getting better at navigating and understanding how it works internally, and I have a Technorati search on Seaside in my newsreader to keep up to date on Seaside news.
After reading Ramon Leon’s A Simple File Based Wiki in Seaside, I wanted to implement another Rails first-time application, but on Seaside this time: the Todo list. To make things more interesting, I decided I would also make it multi-user.
Please note I am in no way an expert on how to do authentication in Seaside. But I took this as an opportunity to play with decorators, which I did not have the opportunity to do previously.
As Ramon did, I will assume you have a working knowledge of Squeak, Smalltalk and Seaside. Unlike Ramon though, I will guide you through the whole thing: models, controllers and views. Sorry, no tests this time around.
Where appropriate, I will contrast Smalltalk vs Ruby, my usual day-time language of choice.
UPDATE: 2007-10-15: Ramon Leon kindly took the time to review the code and gave me a few pointers. The original article documented revision 3, implemented on Seaside 2.7. This is now Seaside 2.7, and I updated the code to include Ramon’s suggestions.
UPDATE: 2007-10-15: The Monticello package is now up to revision 5, after more suggestions from Ramon.
If you want the goods immediately, setup a Monticello repository on http://mirror.teksol.info/monticello/todo and get the Todo package. You need a Squeak image with Seaside pre-installed. I suggest either Ramon Leon’s My squeak image or Damien Cassou’s Squeak’s developer images. On my Ubuntu Linux, I used Damien’s with the Squeak Debian package.
The application’s models
We’ll need a Todo, TodoUser and TodoUserDatabase. Each user will keep a copy of it’s todos, and the database will give us methods to find and register new users. Let’s start with the user’s database:
In case you never noticed, this is a message named #subclass:instanceVariableNames:classVariableNames:poolDictionaries:category, and the receiver is Object. Through the magic of code formatting, this looks like a class declaration, but it’s just another message.
The database object needs a way to initialize itself. We have two ways to do it: either at instance initialization time, or on first access time. Let’s go the latter way, which is what seems most prevalent in Smalltalk / Seaside code I’ve read:
Put this method in protocol private. Then, we need a way to add and remove users:
These go in protocol accessing. Notice #addUser: calls #raiseDuplicateLoginName. Let’s define that immediately:
Put this method in protocol error handling. #addUser: calls another helper method: #findWithLogin:. The implementation looks like this:
Again, put this method in protocol accessing. We’re done with the database side of things. We can now switch to the user itself.
TodoUser model
As the class comment, enter this text:
Instances of myself represent a user with a login and password, as well as a collection of Todo instances.
In protocol accessing, we define basic accessor methods:
Again, note how the todos instance variable is initialized if it wasn’t previously initialized. Equivalent Ruby code uses the ||= operator.
Then, we need to add and remove todos from the user:
Pretty simple, as things go. Put these in the accessing protocol. The final model is the Todo itself.
Todo model
Let’s declare the class:
And the class’ comment:
Instances of myself represent a task that should be done, a todo. Todos are pretty simple: they have a description and a flag that identifies the completion status. Todos also keep track of when they were instantiated and completed.
This time around, we need an #initialize method:
Put this in the initialization protocol. Next, in protocol accessing, we add a couple of basic accessors:
The Seaside UI: controllers and views
To kick things off, I define a new Seaside WAComponent subclass which will be our root component:
We define ourselves a class variable named UserDatabase which will hold an instance of TodoUserDatabase. Let’s give ourselves two accessor methods to the database: one class side and the other instance side. Put this one in accessing, class side:
Again, we see the same pattern: initialize unless already initialized. Back on the instance side, add this method in the accessing protocol:
This is a simple call to the class side version of the method with the same name.
TodoAuthDecorator
To implement authentication, we must wrap the application within a decorator that will take care of these details for it. The decorator’s job is simple: authenticate or register a new user, and when authenticated, show the application instead of the authentication / registration form. I based this implementation on Seaside’s WABasicAuthentication, but mine uses a form instead of the HTTP Basic Access Authentication method. I could have subclassed WABasicAuthentication, but I wanted to learn how to do it manually before I reused code.
Seaside’s decorators have an owner, which is the decorated component. The decorator has a chance to let the component render itself or not, which is what happens later in #renderContentOn:.
Let’s start by declaring the class:
login, password and passwordConfirmation are used to hold the login and password during registration and authentication. I use authenticated as a simple boolean to determine if authentication was successful or not. authenticationMessage and registrationMessage are messages that will be shown to the user (think of Rail’s flash). I begin in the initialization protocol with:
All Seaside decorators that want a chance to render around their owner must provide a #renderContentOn: method. This goes in the rendering protocol:
When authenticated, we render our owner (the decorated component), else we call #renderAuthenticationFormOn:, which looks like this:
Most of this is setting up nested DIVs to create a two-column layout. Styling is handled by the #style method:
#renderAuthenticationFormOn: uses a couple of helper methods:
WATag’s #on:of: message is pretty powerful: it sends the specified message to the specified object. WATextInputTag also sends a mutator message to the object when doing form submissions. The returning user case calls #authenticate on self, which is implemented as follows:
This goes in the actions protocol. When registering, we instead call #register:
If the user doesn’t already exist (as identified through the login), and the password and the confirmation match, we register the new user and immediately authenticate him. Once the user is authenticated, we finally let the decorated component (the decorator’s owner) render itself:
Put this in the rendering protocol. Here, we provide a logout link for authenticated users, as well as show who’s list this is. Then, we call our superclass’ #renderOwnerOn: to let the decorated component render itself. The logout link callsback the #logout method (in the actions protocol):
Above, we called a couple of convenience methods, which obviously go in the convenience protocol:
Lastly, a couple of accessor methods:
TodoApp: our first real component
TodoApp is our root application component. It should thus register itself as a Seaside root. Let’s begin by declaring the class:
Then, class side, in protocol testing, we implement #canBeRoot:
Still class side, in protocol initialization, add:
This registers TodoApp as an application in Seaside, under /seaside/todo. Back on the instance side, in protocol initialization, we implement:
The root component knows that it needs authentication, so it immediately adds a decoration to itself. Seaside requires a component to register itself for backtracking if it’s collection of children will change during it’s lifecycle. Since the user will add and remove todos, our collection of TodoViewer instances will change.
Components render themselves, so put this in protocol rendering:
We render our collection of children, which is the viewers instance variable. Again, we have a callback when creating a new todo. In protocol call/answer, we add:
Again, we have a couple of accessors which are pretty simple:
#addTodo: adds the todo to the user instance, and also registers a new TodoViewer instance. #user: takes care of cleanup in case of logout (aUser isNil), and registers new TodoViewer instances when logging in (aUser notNil).
TodoViewer: A simple model viewer
This class is pretty simple. It’s job is to display a todo and allow it to be marked completed. Let’s declare the class:
We begin by rendering the component:
The call to #class: sets up an HTML class on the paragraph element, to help styling. A couple more rendering methods:
And the usual accessors:
TodoEditor: Create or edit a Todo instance
The current version of the todo app doesn’t use TodoEditor to edit existing todos, but it does use it for creating new instances. As usual, let’s declare the class:
Next, we render the component:
Here is where things get interesting: #on:of: is used to set a callback on the todo instance. Seaside will call #description: of the todo instance to set the value on form post. We don’t need a temporary variable to hold the description in the component: the todo instance takes care of that.
In protocol call/answer, we add the following methods, which are called from #renderContentOn:
And the final accessors:
Total line count: 233 lines, thanks to:
Print this to get the total.
UPDATE 2007-10-15: I counted the lines initially by doing a fileOut and using standard command line tools: cat and wc. Seems I was slightly off.
Todos (pun intended)
There are a couple of things I’d like this todo app to be able to do:
- Use an inline editor to change the description;
- Use Scriptaculous to add a couple of effects;
- Use Ajax to mark completed items;
- Format dates and times;
- Maybe set the user’s timezone and show dates and times using the user’s timezone;
- Date/time formats per-user;
- Enhance security by not storing plain-text versions of passwords in user instances.
This is a toy project. I might never again touch this application.
Recap
One point I’d like to be clear on:
Don’t do that!
I store an unencrypted copy of the password in TodoUser instances. That should never be done. As many others before me, I tried to simplify the code as much as possible. There probably lurks a Password model object in there.
Also, I am no expert on Smalltalk, Squeak or Seaside. There are probably a couple of things I could have done differently, and I hope some people out there might be interested in helping me learn more about Seaside.
I hope you enjoyed this article. There might be more of these coming later. Send me E-Mail at francois@teksol.info or write a comment.
Sorry, comments are closed for this article.