LISP-ON-LINES Drew Crampsie, José Pablo Ezequiel "Pupeno" Fernández Silva Abstract Lisp-On-Lines is a very useful module that works on top of the UnCommon Web framework to do rapid developing of complex data-driven web appilcations (on Common Lisp, of course). 1 Introduction Lisp-On-Lines was founded and developed and continues to be developed and mantained by Drew Crampsie. 1.1 Conventions The conventions used in this manual are: * Code is shown in a monospace font. When it is expected that the user is working in an interactive environment what the user should type appears as bold, while the computer result appears non-bold, for example: > (+ 5 10) 15 * Names of people or products are show as small caps, like Drew Crampsie or Lisp-On-Lines. * Sections marked with ToDo require further revision. ToDo: Add more conventions as they are needed, possible classes of text: names of concepts, name of programming entities, like variables, functions, etc (which are embedded in text, should they be shown monospaced ?). 2 Components Meta Model Protocol A Protocol for introspection on relational objects. Mewa Presentations A Mewa-likehttp://www.adrian-lienhard.ch/files/mewa.pdf layer for UncommonWebhttp://common-lisp.net/project/ucw/ Presentations. 3 Example First we start with the data model. The Meta Model Protocol (MMP) is used to provide information on the data objects and how they relate to one another. Its is currently implemented as a layer over CLSQLhttp://clsql.b9.com/, although support is planned for other backends (CLOS, Elephant[4], whatever). The MMP shares its definition syntax with CLSQL's Object Oriented Data Definition Language (OODDL)http://clsql.b9.com/manual/ref-ooddl.html. The macro to define view-classes is named DEF-VIEW-CLASS/META, and takes the same arguments as DEF-VIEW-CLASS from CLSQL. For the purposes of this simple example, we will only need two functions from the MMP beyond what CLSQL provides : LIST-SLOTS and LIST-SLOT-TYPES[5]. We'll define a simple class to hold a user. > (def-view-class/meta user () ((userid :initarg :userid :accessor userid :type integer :db-kind :key) (username :initarg :username :accessor username :type string :db-kind :base) (password :initarg :password :accessor password :type string :db-kind :base))) and now we create a user: > (defparameter user (make-instance 'user :userid 1 :username "drewc" :password "p@ssw0rd")) We can see the slots of users running: > (lisp-on-lines::list-slots user) (USERID USERNAME PASSWORD) or the types with: > (lisp-on-lines::list-slot-types user) ((USERID INTEGER) (USERNAME STRING) (PASSWORD STRING)) To see the default attributes of a classIs this correct ? Drew, please, check. we run. > (lisp-on-lines::default-attributes user) ((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)) To set the attributes of a class to the default values we use: > (lisp-on-lines::set-default-attributes user) ((USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD)) which takes an object of the class we are working with. This is going to be change so we can do this action directly on the class. It is on the TODO file. Class attributes? > (lisp-on-lines::find-class-attributes user) (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) NIL) note that the mewa functions (find-attribute, set-attribute etc) can take either an instance, or a class-name as a symbol: > (lisp-on-lines::find-class-attributes 'user) (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) NIL) > (lisp-on-lines::find-class-attributes (make-instance 'user)) (USER (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) NIL) Using that information, we have enough to create an interface to the object. UnCommon Web includes a powerful presentation systemTo see this system in action, we strongly recomend to study the presentations example which comes with UnCommon Web. Reading components/presentations.lisp can help understand a lot about how presentations are built. , but it is not dynamic enough for some of the most advanced applications. Mewa defines an approach to presentations that solves that problem, but the paper is written from a Smalltalk point of view. A mixture of the two , Mewa Presentations(MP), is described here. MP introduces to UnCommon Web the concept of attributes. An attribute is essentially a named version of the DEFPRESENTATION slot-like arguments, for example in : > (defpresentation person-editor (object-presentation) ((string :label "First Name" :slot-name 'first-name :max-length 30))) the (string :label "First Name" ...) form is an attribute definiton. Attributes are accessed through FIND-ATTIRIBUTES, and are composed at run time (where the UnCommon Web's presentation system is done at compile time) to display the object. This allows a very flexible system of displaying objects which is reminiscent of CSSDrew Crapmsie discovered this, rather than invent or design it, so there are some rough edges, but its a good start. . Its much easier to show this than to tell. Lets present our user class. Currently in UnCommon Web, you'd define a presentation as such : > (defpresentation user-presentation (object-presentation) ((INTEGER :LABEL "USERID" :SLOT-NAME USERID) (STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD))) which could be presented using PRESENT-OBJECT : > (present-object user :using 'user-presentation) The equivalent approach using mewa presentations is actually longer and more verbose(!) but it serves to demonstrate how the MP system works. Mewa Presentations adds a set of attributes to a class, keyed off the class name. Attributes are inherited, so if you define an attribute on T, you can use it with any class. MP stores named attributes keyed on a class name. To achieve the same functionality as the above using mp would look like this : > (setf (lisp-on-lines::find-attribute 'user :viewer)Isn't this too imperative (in contrast to functional, lispy). '(lisp-on-lines::mewa-object-presentation :attributes (userid username password) :global-properties (:editablep nil))) (:VIEWER MEWA-OBJECT-PRESENTATION :ATTRIBUTES (USERID USERNAME PASSWORD) :GLOBAL-PROPERTIES (:EDITABLEP NIL)) > (setf (lisp-on-lines::find-attribute 'user 'userid)Are this setfs to 'userid, 'username and 'password needed ? I (Pupeno) inspected they contents at of this moment and they seem to already contain what they are being set to. '(integer :label "userid" :slot-name userid)) (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) > (setf (lisp-on-lines::find-attribute 'user 'username) '(STRING :LABEL "USERNAME" :SLOT-NAME USERNAME)) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) > (setf (lisp-on-lines::find-attribute 'user 'password) '(STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD)) (PASSWORD STRING :LABEL "USERNAME" :SLOT-NAME PASSWORD) > (lisp-on-lines::find-class-attributes 'user) (USER (:VIEWER MEWA-OBJECT-PRESENTATION :ATTRIBUTES (USERID USERNAME PASSWORD) :GLOBAL-PROPERTIES (:EDITABLEP NIL)) (PASSWORD STRING :LABEL "PASSWORD" :SLOT-NAME PASSWORD) (USERNAME STRING :LABEL "USERNAME" :SLOT-NAME USERNAME) (USERID INTEGER :LABEL "USERID" :SLOT-NAME USERID) NIL) this is all turned into a UnCommon Web presentation at runtime using MAKE-PRESENTATION, for example, the following code should be enough to show what's built so far attached to the examples application: > (defcomponent lol-example (window-component) ()) > (defmethod render-on ((res response) (lol-example lol-example)) (<:h1 "User") ( (defentry-point "lol.ucw" (:application *example-application*) () (call 'lol-example)) As you'll see, nothing is exported from the LISP-ON-LINES package. If you wish to use LOL in your own package (or in UCW-USER or whatever), you simply need to use the MEWA and META-MODEL packages. SET-ATTRIBUTE can be used in place of (setf (find-attribute ...)) when you want to "inherit" the properties of an existing attribute definition : LISP-ON-LINES> (set-attribute 'user 'password '(string :label "password: (must be at least 8 chars)")) (PASSWORD STRING :LABEL "password: (must be at leat 8 chars)" :SLOT-NAME PASSWORD) Now we want to create a presentation with which to edit the username. we will use the existing attributes on a subclass of mewa-object-presetation : > (defcomponent user-editor (mewa-object-presentation) () (:default-initargs :attributes '((username :label "Enter your New Username") password) :global-properties '(:editablep t))) USER-EDITOR LISP-ON-LINES> (setf (find-attribute 'user :editor) '(user-editor)) (:EDITOR USER-EDITOR) which we then can display below our earlier example : (defmethod render-on ((res response) (e presentations-index)) " As you'll see, nothing is exported from the LISP-ON-LINES package. if you wish to use LOL in your own package (or in UCW-USER or whatever), you simply need to use the MEWA and META-MODEL packages" ( (clsql:connect '("localhost" "geo" "geo" "geogeo")) which connect us to the server on localhost, to the database geo as user "geo" with password "geogeo" (this is not a smart way to generate password, don't do this). To have a nice SQL environment, we also want: > (clsql:locally-enable-sql-reader-syntax) > (setf clsql:*default-caching* nil) Actually, it is more than a nice environmnet, without those lines the rest of the code won't work. On the geo database, there's a table called product which has the following structure: CREATE TABLE product ( id serial NOT NULL, name text NOT NULL, details text, description text, cost double precision, CONSTRAINT product_cost_check CHECK ((cost > (0)::double precision)) ); ALTER TABLE ONLY product ADD CONSTRAINT product_name_key UNIQUE (name); ALTER TABLE ONLY product ADD CONSTRAINT product_pkey PRIMARY KEY (id); ToDo: express the table structure in a better way. Now we'll create the class that represents a product, mirroring the database structure: > (lisp-on-lines::def-view-class/table "product") and then we generate the default attributes (from product's slots) and assign it to product: > (lisp-on-lines::set-default-attributes (make-instance 'product))set-default-attributes is marked to be renamed to set-generated-attributes. As you can see, we instantiate product to pass it to set-default-attributes, because it expects an object instead of a class. We don't need the object anymore, so we don't save any reference to it. In the future we might have a set-default-attributes that can use a class directly. Now we set a the attribute :viewer to contain the mewa-object-presentation exposing the attributes we like the user to work with: > (setf (lisp-on-lines::find-attribute (make-instance 'product) :viewer) '(lisp-on-lines::mewa-object-presentation :attributes (name details description cost) :global-properties (:editablep nil))) The last parameter is a list of properties that will be applied to each attribute. 5 Yet Another Example . Drew Crampsie Posted the following to comp.lang.lisp .. it just might help until he writes some real documentation. I've written a system that generates presentations for database objects based on the type and relation information in the system catalog. Its based on MewaMewa : Meta-level Architecture for Generic Web-Application Construction http://map1.squeakfoundation.org/sm/package/32c5401f-fa30-4a2b-80c8-1006dd462859 clsql + postgres and the UCW presentation components. This is the code to add a new contact to the system. (screenshot pr0n follows). In the RENDER-ON method of my front-page i have : (let ((p (make-instance 'person :person-type-code nil))) (<:as-html "Add Person :") (adjuster-id) '(ucw::has-very-many :label "Claims as Adjuster" :slot-name claim->adjuster-id ) ) (set-attribute 'person 'policy->agent-id '(ucw::has-very-many :label "Policies as Agent")) (defcomponent new-person (person-display) () (:default-initargs :attributes '(first-name last-name company-name))) (defcomponent person-chooser (mewa::mewa-list-presentation) () (:default-initargs :attributes '(first-name last-name company-name address city person-type-code) :global-properties '(:editablep nil) :editablep nil :deleteablep nil)) (defmethod render-on :wrapping ((res response) (self person-chooser)) (<:p (<:as-html "Similar contact(s) in database. You can :") (<:ul (<:li (<:as-html "Select one of the contacts below")) (<:li (