Weblocks Tutorial Blog #1

This is going to be kind of freeform to start; sorry.

OK, new app. Using
http://weblocks.viridian-project.de/installation as the starting
point.
Turns out you need to actually load weblocks to have
it set up the app template. :-) So, run "clbuild lisp" and:

* (asdf:oos 'asdf:load-op 'weblocks)
    [much output, returns NIL]
* (wop:make-app 'recipes "/home/rlpowell/programming/weblocks_tutorial/version1/")
    [it requires an absolute path]

creating directory: /home/rlpowell/programming/weblocks_tutorial/version1/recipes/
creating directory: /home/rlpowell/programming/weblocks_tutorial/version1/recipes/src/
creating directory: /home/rlpowell/programming/weblocks_tutorial/version1/recipes/conf/
creating directory: /home/rlpowell/programming/weblocks_tutorial/version1/recipes/data/

RECIPES has been created.
Please add '/home/rlpowell/programming/weblocks_tutorial/version1/recipes/' to asdf:*central-registry* before you proceed.
Happy hacking!
NIL
* (push #p"/home/rlpowell/programming/weblocks_tutorial/version1/recipes/" asdf:*central-registry*)


OK, so, looking around in a seperate window, what have I got?

Startup files, "recipes.asd" and "recipes.lisp".

A "pub/" directory with a bunch of static assets (javascript and pictures).

A "conf/" directory to define the data stores (currently a single CL-Prevalence store), and "data/" to store them.

An "src/" directory with a session initializer that's basically a hello world app.

OK, first to fire it up:

* (asdf:oos 'asdf:load-op 'recipes)
* (recipes:start-recipes :port 3455)


As expected, a hello world app (actually "Happy Hacking!"). There's also a button to reset session information in the bottom left, presumably to make debugging easier (especially if you haven't put any login/logout functionality in yet). Just out of curiousity, let's see if I can easily turn it off.

There's a ":debug t" in the defwebapp call in recipes.lisp; that seems a likely candidate. So, set that to nil, paste it into "clbuild lisp", and restart:

* (recipes:stop-recipes)
NIL
* (recipes:start-recipes :port 3455)
#<RECIPES {1003FFEBA1}>


and it's gone. Yay. (Now I'm going to put it back; it'll come in handy, I just wanted to make sure I understood where it was coming from. Note that this could probably have been done without the restart, but I didn't feel like digging into the how of it.)

OK, now let's actually get something application-like in place. Let's start with something very simple: recipies have a name and a description.

Since the point here is not a lesson in source tree layout style, this is getting added directly to src/init-session.lisp. In fact, we're replacing that file's contents with this:

(in-package :recipes)

(defclass recipe ()
  ((id :accessor recipe-id)
   (name :accessor recipe-name
         :initarg :name)
   (description :accessor recipe-description
                :initarg :description)))

; Define what a new user session should see when they come to the
; site, which is just a list of the recipe objects.
(defun init-user-session (comp)
  ; Clean out the store; this isn't really necessary, but the store
  ; does persist across totally separate runs, so if you muck with the
  ; class definition it'll explode when it tries to load the old
  ; stuff, so it's just easier.
  (dolist
   (obj (find-persistent-objects *default-store* 'recipe))
   (delete-persistent-object *default-store* obj))

  ; Put some example objects in the store
  (persist-objects *default-store*
                   (list
                    (make-instance 'recipe
                                   :name "Chocolate Chip Cookies"
                                   :description "Mmmmm.")
                    (make-instance 'recipe
                                   :name "Caramel"
                                   :description "Tasty!")))

  (setf (composite-widgets comp)
        (make-instance 'datalist
                       :data-class 'recipe)))


FIXME: No more composite widgets; just widget-children (defined in
defclass widget)

OK, so, there's a fair bit of code there.

The recipe definition is pretty straightforward. Note that
CL-Prevalence, the
data store we're using because it's simple and easy and this is just
a tutorial, requires that objects have an id slot.

Then we clean out the store (makes repeat testing easier) and put a
couple of sample objects in it.

init-user-session is the root of all code in your Weblocks application. It takes
a single argument, the root widget.

Weblocks organizes everything as widgets, which are basically self-contained bits of web presentation
(a form, a list, etc). Some widgets can contain other widgets; the widget class used for
that purpose is the "composite" widget. The root widget, obviousy, is of composite class.

Composite widgets just have a list of widgets inside them; this is set by the (composite-widgets...) slot accessor.

Normally this is set to a list as I said, but it can be set to a single widget, too, which
is what I did above, for simplicity.

What I set it to is an instance of the datalist widget class. This presents a list of objects and,
optionally, lets you drill down into them: click on them to see detailed information and possibly edit them and suchlike.

I then told it which class it should be displaying with ":data-class 'recipe". You can tell it which members
to use, but again, simplicity. What it's going to do is just grab every mumber of class "recipe" in the
default data store and display them.

Which works just fine: FIXME: PIC

It's pretty ugly, though. First simple fix: let's drop the id field.

(defview recipe-data-view (:type data :inherit-from '(:scaffold recipe))
         (id :hidep t))

(defun init-user-session (comp)
  ...
  (setf (composite-widgets comp)
        (make-instance 'datalist
                       :item-data-view 'recipe-data-view
                       :data-class 'recipe)))


FIXME: fill in the ... above, perhaps by making the setf a seperate function or something so it can be talked about independently

":item-data-view 'recipe-data-view" says "use recipe-data-view as the view definition for presenting the items".

defview, obviously, defines views. Views are used by widgets to pick what data to present, and how.

The type is used to determine the view class that will be used. There are four currently:

  • data-view is used for presenting a single item as a simple list of slots
  • sequence-view is used for presenting a list of items with sorting and such; the datalist widget uses it for ordering, but not presentation. It's basically not ever directly used
  • table-view is used for presenting a bunch of items as a table
  • form-view is used for presenting an item as a form for editing


":inherit-from '(:scaffold recipe)" just means "make entries for each slot in this object". It's the default
view behaviour if you don't specify a view.

Then comes a list of field names and keywords to modify how the scaffold version treats the various fields. The various options here are defined in the view-field class (FIXME: detail them), but all that code does is hide the id field.

There's a totally different way to do this: instead of using the scaffold and hiding what we don't want, we can make it ourselves and not include what we don't want, like so:

(defview recipe-data-view (:type data)
         (name)
         (description))


FIXME: Work up to this, mostly for illustrative purposes:

(defview recipe-data-view (:type data :inherit-from '(:scaffold recipe))
         (id :hidep t))

(defun init-user-session (comp)
  (let
    ((grid
       (make-instance
         'datagrid
         :allow-drilldown-p t
         :data-class 'recipe
         ))
     (dlist
       (make-instance
         'datalist
         :allow-drilldown-p t
         :data-class 'recipe)))

    (setf (dataseq-on-drilldown dlist)
          (cons
            'group
            (lambda (current-widget item)
              (do-widget
                current-widget
                grid))))

    (setf (dataseq-on-drilldown grid)
          (cons
            'single
            (lambda (current-widget item)
              (setf (dataseq-on-query dlist)
                    (lambda (widget sorting pagination &key countp)
                      (if countp 1
                        (list item))))
              (do-widget
                current-widget
                dlist))))

    (setf (composite-widgets comp) grid)))



FIXME: Work up to something decent, and leave it running.