3 Drew Crampsie, José Pablo Ezequiel "Pupeno" Fernández Silva
7 Lisp-On-Lines is a very useful module that works on top
8 of the UnCommon Web framework to do rapid developing of
9 complex data-driven web appilcations (on Common Lisp,
14 Lisp-On-Lines was founded and developed and continues
15 to be developed and mantained by Drew Crampsie.
19 The conventions used in this manual are:
21 * Code is shown in a monospace font. When it is
22 expected that the user is working in an interactive
23 environment what the user should type appears as
24 bold, while the computer result appears non-bold, for example:
30 * Names of people or products are show as small caps,
31 like Drew Crampsie or Lisp-On-Lines.
33 * Sections marked with ToDo require further revision.
35 ToDo: Add more conventions as they are needed, possible
36 classes of text: names of concepts, name of programming
37 entities, like variables, functions, etc (which are
38 embedded in text, should they be shown monospaced ?).
42 Meta Model Protocol A Protocol for introspection on
45 Mewa Presentations A Mewa-likehttp://www.adrian-lienhard.ch/files/mewa.pdf layer for UncommonWebhttp://common-lisp.net/project/ucw/
50 First we start with the data model. The Meta Model
51 Protocol (MMP) is used to provide information on the
52 data objects and how they relate to one another. Its is
53 currently implemented as a layer over CLSQLhttp://clsql.b9.com/, although
54 support is planned for other backends (CLOS,
55 Elephant[4], whatever).
57 The MMP shares its definition syntax with CLSQL's
58 Object Oriented Data Definition Language (OODDL)http://clsql.b9.com/manual/ref-ooddl.html. The
59 macro to define view-classes is named
60 DEF-VIEW-CLASS/META, and takes the same arguments as
61 DEF-VIEW-CLASS from CLSQL. For the purposes of this
62 simple example, we will only need two functions from
63 the MMP beyond what CLSQL provides : LIST-SLOTS and
66 We'll define a simple class to hold a user.
68 > (def-view-class/meta user ()
70 ((userid :initarg :userid :accessor userid :type
71 integer :db-kind :key)
73 (username :initarg :username :accessor username
74 :type string :db-kind :base)
76 (password :initarg :password :accessor password
77 :type string :db-kind :base)))
79 and now we create a user:
81 > (defparameter user (make-instance 'user :userid 1
85 :password "p@ssw0rd"))
87 We can see the slots of users running:
89 > (lisp-on-lines::list-slots user)
91 (USERID USERNAME PASSWORD)
95 > (lisp-on-lines::list-slot-types user)
97 ((USERID INTEGER) (USERNAME STRING) (PASSWORD STRING))
99 To see the default attributes of a classIs this correct ? Drew, please, check. we run.
101 > (lisp-on-lines::default-attributes user)
103 ((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
105 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
107 (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD))
109 To set the attributes of a class to the default values
112 > (lisp-on-lines::set-default-attributes user)
114 ((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
116 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
118 (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD))
120 which takes an object of the class we are working with.
121 This is going to be change so we can do this action
122 directly on the class. It is on the TODO file.
126 > (lisp-on-lines::find-class-attributes user)
128 (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)
130 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
132 (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
136 note that the mewa functions (find-attribute,
137 set-attribute etc) can take either an instance, or a
138 class-name as a symbol:
140 > (lisp-on-lines::find-class-attributes 'user)
142 (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)
144 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
146 (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
150 > (lisp-on-lines::find-class-attributes (make-instance 'user))
152 (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)
154 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
156 (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
160 Using that information, we have enough to create an
161 interface to the object. UnCommon Web includes a
162 powerful presentation systemTo see this system in action, we strongly recomend to
163 study the presentations example which comes with
164 UnCommon Web. Reading components/presentations.lisp can
165 help understand a lot about how presentations are built.
166 , but it is not dynamic enough for some of the most
167 advanced applications. Mewa defines an approach to
168 presentations that solves that problem, but the paper
169 is written from a Smalltalk point of view. A mixture of
170 the two , Mewa Presentations(MP), is described here.
172 MP introduces to UnCommon Web the concept of
173 attributes. An attribute is essentially a named version
174 of the DEFPRESENTATION slot-like arguments, for example
177 > (defpresentation person-editor (object-presentation)
179 ((string :label "First Name" :slot-name 'first-name
182 the (string :label "First Name" ...) form is an
183 attribute definiton. Attributes are accessed through
184 FIND-ATTIRIBUTES, and are composed at run time (where
185 the UnCommon Web's presentation system is done at
186 compile time) to display the object. This allows a very
187 flexible system of displaying objects which is
188 reminiscent of CSSDrew Crapmsie discovered this, rather than invent or
189 design it, so there are some rough edges, but its a
193 Its much easier to show this than to tell. Lets present
194 our user class. Currently in UnCommon Web, you'd define
195 a presentation as such :
197 > (defpresentation user-presentation (object-presentation)
199 ((INTEGER :LABEL "USERID" :SLOT-NAME USERID)
201 (STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
203 (STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)))
205 which could be presented using PRESENT-OBJECT :
207 > (present-object user :using 'user-presentation)
209 The equivalent approach using mewa presentations is
210 actually longer and more verbose(!) but it serves to
211 demonstrate how the MP system works.
213 Mewa Presentations adds a set of attributes to a class,
214 keyed off the class name. Attributes are inherited, so
215 if you define an attribute on T, you can use it with
218 MP stores named attributes keyed on a class name. To
219 achieve the same functionality as the above using mp
220 would look like this :
222 > (setf (lisp-on-lines::find-attribute 'user :viewer)Isn't this too imperative (in contrast to functional, lispy).
224 '(lisp-on-lines::mewa-object-presentation
226 :attributes (userid username password)
228 :global-properties (:editablep nil)))
230 (:VIEWER MEWA-OBJECT-PRESENTATION
234 (USERID USERNAME PASSWORD)
240 > (setf (lisp-on-lines::find-attribute 'user 'userid)Are this setfs to 'userid, 'username and 'password
241 needed ? I (Pupeno) inspected they contents at of this
242 moment and they seem to already contain what they are
245 '(integer :label "userid" :slot-name userid))
247 (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
249 > (setf (lisp-on-lines::find-attribute 'user 'username)
251 '(STRING :LABEL "USERNAME" :SLOT-NAME USERNAME))
253 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
255 > (setf (lisp-on-lines::find-attribute 'user 'password)
257 '(STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD))
259 (PASSWORD STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD)
261 > (lisp-on-lines::find-class-attributes 'user)
265 (:VIEWER MEWA-OBJECT-PRESENTATION
269 (USERID USERNAME PASSWORD)
275 (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)
277 (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)
279 (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID)
283 this is all turned into a UnCommon Web presentation at
284 runtime using MAKE-PRESENTATION, for example, the
285 following code should be enough to show what's built so
286 far attached to the examples application:
288 > (defcomponent lol-example (window-component)
292 > (defmethod render-on ((res response) (lol-example lol-example))
296 (<ucw:render-component :component
297 (lisp-on-lines::make-presentation user :type :viewer)))
299 > (defentry-point "lol.ucw" (:application
300 *example-application*) ()
304 As you'll see, nothing is exported from the
305 LISP-ON-LINES package. If you wish to use LOL in your
306 own package (or in UCW-USER or whatever), you simply
307 need to use the MEWA and META-MODEL packages.
309 SET-ATTRIBUTE can be used in place of (setf
310 (find-attribute ...)) when you want to "inherit" the
311 properties of an existing attribute definition :
313 LISP-ON-LINES> (set-attribute 'user 'password '(string
314 :label "password: (must be at least 8 chars)"))
320 "password: (must be at leat 8 chars)"
326 Now we want to create a presentation with which to edit
327 the username. we will use the existing attributes on a
328 subclass of mewa-object-presetation :
330 > (defcomponent user-editor (mewa-object-presentation)
336 :attributes '((username :label "Enter your New
339 :global-properties '(:editablep t)))
343 LISP-ON-LINES> (setf (find-attribute 'user :editor)
346 (:EDITOR USER-EDITOR)
348 which we then can display below our earlier example :
350 (defmethod render-on ((res response) (e presentations-index))
354 As you'll see, nothing is exported from the
355 LISP-ON-LINES package.
357 if you wish to use LOL in your own package (or in
358 UCW-USER or whatever),
360 you simply need to use the MEWA and META-MODEL
363 (<ucw:render-component :component
364 (lisp-on-lines::make-presentation lisp-on-lines::user
367 (<ucw:render-component :component
368 (lisp-on-lines::make-presentation lisp-on-lines::user
371 that should give you some idea on how it works .. ask
372 me when you get confused :)
376 This is Pupeno's view of how to do rapid developing of
377 a database-driven web application. It currently is
378 going to assume a very specific case but latter it may
381 We first start with a PostgreSQL connection of CLSQL
382 which is set up with one line:
384 > (clsql:connect '("localhost" "geo" "geo" "geogeo"))
386 which connect us to the server on localhost, to the
387 database geo as user "geo" with password "geogeo" (this is
388 not a smart way to generate password, don't do this).
389 To have a nice SQL environment, we also want:
391 > (clsql:locally-enable-sql-reader-syntax)
393 > (setf clsql:*default-caching* nil)
395 Actually, it is more than a nice environmnet, without
396 those lines the rest of the code won't work.
398 On the geo database, there's a table called product
399 which has the following structure:
401 CREATE TABLE product (
411 cost double precision,
413 CONSTRAINT product_cost_check CHECK ((cost >
414 (0)::double precision))
418 ALTER TABLE ONLY product ADD CONSTRAINT
419 product_name_key UNIQUE (name);
421 ALTER TABLE ONLY product ADD CONSTRAINT product_pkey
424 ToDo: express the table structure in a better way.
426 Now we'll create the class that represents a product,
427 mirroring the database structure:
429 > (lisp-on-lines::def-view-class/table "product")
431 and then we generate the default attributes (from
432 product's slots) and assign it to product:
434 > (lisp-on-lines::set-default-attributes (make-instance
435 'product))set-default-attributes is marked to be renamed to
436 set-generated-attributes.
438 As you can see, we instantiate product to pass it to
439 set-default-attributes, because it expects an object
440 instead of a class. We don't need the object anymore,
441 so we don't save any reference to it. In the future we
442 might have a set-default-attributes that can use a
443 class directly. Now we set a the attribute :viewer to
444 contain the mewa-object-presentation exposing the
445 attributes we like the user to work with:
447 > (setf (lisp-on-lines::find-attribute (make-instance
450 '(lisp-on-lines::mewa-object-presentation
452 :attributes (name details description cost)
454 :global-properties (:editablep nil)))
456 The last parameter is a list of properties that will be
457 applied to each attribute.
459 5 Yet Another Example .
461 Drew Crampsie Posted the following to comp.lang.lisp ..
462 it just might help until he writes some real
465 I've written a system that generates presentations for
466 database objects based on the type and relation
467 information in the system catalog. Its based on MewaMewa : Meta-level Architecture for Generic
468 Web-Application Construction
485 http://map1.squeakfoundation.org/sm/package/32c5401f-fa30-4a2b-80c8-1006dd462859
486 clsql + postgres and the UCW presentation components.
488 This is the code to add a new contact to the system.
489 (screenshot pr0n follows).
491 In the RENDER-ON method of my front-page i have :
495 (let ((p (make-instance 'person :person-type-code nil)))
497 (<:as-html "Add Person :")
499 (<ucw:render-component
501 :component (make-presentation
507 :initargs '(:attributes
512 (<ucw:submit :action (new-person self p) :value "add"))
518 This creates a drop-down list of person-types and an
519 "add" button which calls NEW-PERSON :
521 (defaction new-person ((self component) person)
525 Take a PERSON with a user-defined PERSON-TYPE-CODE,
527 * Prompt the user for a FIRST-NAME, LAST-NAME and/or
530 * Search for similar PERSONs in the database.
532 * If they exist, ask the user to select one or continue
534 * otherwise, just continue editing the person"
538 (call-component self (make-presentation
544 :initargs '(:global-properties
551 (call-component self (make-presentation
554 (find-or-return-named-person self named-person)
560 (defaction find-or-return-named-person ((self
565 If any similiar contacts exist in the database,
567 select one or continue with the current person
569 PERSON must have FIRST-NAME, LAST-NAME and COMPANY-NAME bound."
571 (let ((instances (sql-word-search person 'first-name
572 'last-name 'company-name)))
576 (call-component self (make-presentation
580 :type 'person-chooser
584 `(:instances ,instances)))
588 You can hardly tell it's a web application ... there is
589 no checking of CGI params etc... just nice code in the
590 order i wanted to write it.
594 * http://tech.coop/img/screenshots/select-person-type.jpg
596 * http://tech.coop/img/screenshots/enter-person-name.jpg
598 * http://tech.coop/img/screenshots/select-similar-contacts.jpg
600 * http://tech.coop/img/screenshots/edit-person-details.jpg
602 * http://tech.coop/img/screenshots/view-recent-changes.jpg
604 All of the code used to create the presentations for
605 this is below my sig. I do eventually plan to release
606 the presentation system as Free Software, it just needs
607 a little cleaning up. E-mail me for a sneak peak.
613 drewc at tech dot coop
615 "Never mind the bollocks -- here's the sexp's tools."
617 -- Karl A. Krueger on comp.lang.lisp
621 (def-view-class/table "person")
625 (set-default-attributes (make-instance 'person)
629 (defcomponent person-display (mewa::two-column-presentation)
635 (defcomponent one-line-person (mewa::mewa-one-line-presentation)
639 (:default-initargs :attributes '(first-name last-name
644 (setf (find-attribute 'person :one-line) '(one-line-person))
648 (set-attribute 'person 'person-type-code '(code-select
653 (set-attribute 'person 'province-state-code
654 '(code-select :category 2))
658 (setf (find-attribute 'person :viewer) '(person-display
659 :global-properties (:editablep nil)))
663 (set-attribute 'person :editor '(person-display
664 :global-properties (:editablep t)))
668 (setf (find-attribute 'person 'claim->adjuster-id)
669 '(ucw::has-very-many :label "Claims as Adjuster"
670 :slot-name claim->adjuster-id ) )
674 (set-attribute 'person 'policy->agent-id
675 '(ucw::has-very-many :label "Policies as Agent"))
679 (defcomponent new-person (person-display)
685 :attributes '(first-name last-name company-name)))
689 (defcomponent person-chooser (mewa::mewa-list-presentation)
695 :attributes '(first-name
707 :global-properties '(:editablep nil)
715 (defmethod render-on :wrapping ((res response) (self
718 (<:p (<:as-html "Similar contact(s) in database. You
723 (<:li (<:as-html "Select one of the contacts below"))
725 (<:li (<ucw:a :action (answer (instance self))
727 (<:as-html "Continue, adding a
734 (defaction ok ((self new-person) &optional arg)
736 (declare (ignore arg))
738 (answer (instance self)))
742 (defmethod sql-word-search ((instance
743 standard-db-object) &rest slots)
747 (loop for slot in slots
749 nconc (split-sequence #\Space
750 (slot-value instance slot)))))
752 (select (class-name (class-of instance))
754 :where (sql-or (mapcar #'(lambda (x)
762 (mapcar #'(lambda (y)
767 (sql-slot-value 'person y)
770 (format nil "%~a%" x)))