;; -*- lisp -*- (in-package #:ucw-user) ;;;; * Introduction ;;;; This tutorial will show you how to create web applications with ;;;; UnCommon Web (UCW). During this tutorial we will create two ;;;; distinct, but functionaly similar, wikis. The first, "regular ;;;; style," will be developed using the same mind set and ;;;; organization as you would use with any other web framework ;;;; (though it will use as much of UCW as possible). The second ;;;; wiki, "UCW style," will be an idiomatic UCW application. ;;;; We've named the urls and classes in such a fashion that the two ;;;; interfaces can coexist within the same image. You can try jumping ;;;; back and forth between the two styles and compare how they act. ;;;; ** Getting Started ;;;; At some point we may add minimal installation instructions here, ;;;; until then we'll just assume that you've already downloaded, ;;;; built and setup ucw. ;;;; To load the wiki just load this file after you've started ;;;; UCW. This wiki depends on cl-ppcre and the ucw example ;;;; application. ;;;; * The wiki backend ;;;; Both of our wikis will have very similar functionality: we will ;;;; allow users to view and edit pages; when viewing a page words ;;;; written in StudlyCaps will be converted into hyperlinks to like ;;;; named wiki pages; pages will be editable by anyone at any time; ;;;; every change will record the name of the author and a brief ;;;; summary of the changes. ;;;; We won't worry about "RecentChanges" or search functionality ;;;; (though this may be added later). (defvar *wiki* (make-hash-table :test 'equal)) (defun find-wiki-page (page-name) (gethash page-name *wiki*)) (defun (setf find-wiki-page) (page page-name) (setf (gethash page-name *wiki*) page)) (defclass wiki-page () ((page-name :accessor page-name :initarg :page-name) (contents :accessor contents :initarg :contents :initform ""))) (defmacro defwiki-page (name body) (rebinding (name) `(unless (find-wiki-page ,name) (setf (find-wiki-page ,name) (make-instance 'wiki-page :page-name ,name :contents ,body))))) ;;;; Both of the wikis use "WelcomePage" as the default page name, by ;;;; creating the page here we simplify some of the control flow. (defwiki-page "WelcomePage" "Welcome to the wiki.") (defclass wiki-edit () ((author :accessor author :initarg :author :initform nil) (summary :accessor summary :initarg :summary :initform nil) (contents :accessor contents :initarg :contents)) (:documentation "An object representing a single change to a single wiki page.")) (defun update-wiki-page (page-name change) (symbol-macrolet ((page (find-wiki-page page-name))) (unless page (setf page (make-instance 'wiki-page :page-name page-name))) (setf (contents page) (contents change)) page)) ;;;; * Regular Style ;;;; ** view.ucw ;;;; The view.ucw page is used for viewing the contents of a wiki ;;;; page. The user specifies which page they're insterested in using ;;;; the "page-name" HTTP request parameter. If no page-name is ;;;; specified we default to "WelcomePage," if the specified page-name ;;;; does not exist we assume the user wants to edit (in this case ;;;; create) the page. ;;;; *** view.ucw - Introducing defentry-point (defentry-point "view.ucw" (:application *example-application*) ((page-name "WelcomePage")) (if (find-wiki-page page-name) (call 'view-wiki-page :page-name page-name) (call 'ucw::redirect-component :target (strcat "edit.ucw?page-name=" page-name)))) ;;;; DEFENTRY-POINT is UCW's way of associating a piece of code to a ;;;; particular url, it's analogous to handlers or servlet definitions ;;;; in other frameworks. ;;;; In this case we are creating an entry-point named "view.ucw" and ;;;; tying it to the example-app application. The entry-point takes ;;;; one parameter: page-name. If the page-name parameter is not ;;;; passed in the request we will use "WelcomePage." ;;;; This entry-point is very simple, if the requested wiki-page ;;;; exists then we should just view it using the page (aka component) ;;;; view-wiki-page, if the requested page doesn't exist then we'll ;;;; just pretend that the user actually wants to edit the page. ;;;; What is the "effective" url for this entry point? Since ;;;; example-app application's url-prefix is "/ucw/examples/" we ;;;; simply concatentate the entry-point's name and the url-prefix to ;;;; get "/ucw/examples/view.ucw". The extension needn't be ".ucw" it ;;;; can be anything you want or nothing at all. When using the ;;;; araneida or aserve backends the entry-point's url is ;;;; automatically registered with the server, with the mod_lisp ;;;; backend you must manually configure apache to send requests for ;;;; this entry point to ucw. ;;;; *** view-wiki-page - Introducing components ;;;; How do we tell UCW to show the user some html? While we could ;;;; just litter our entry-point with (progn (write-line "") ;;;; ...) we're not going to, it's bad style and UCW is makes that ;;;; more difficult than it could be. ;;;; What we're supposed to do is hand off our request to a component ;;;; and let it deal with the nitty gritty html stuff. Here's the form ;;;; which defines the view-wiki-page component: (defcomponent view-wiki-page (tal-component) ((page-name :accessor page-name :initarg :page-name)) (:default-initargs :template-name "wiki/view.tal")) (defmethod tal-component-environment nconc ((page view-wiki-page)) (yaclml:tal-env 'contents (cl-ppcre:regex-replace-all "([A-Z][a-z]+){2,}" (contents (find-wiki-page (page-name page))) "\\&"))) ;;;; Notice how much the defcomponent macro looks like defclass, ;;;; that's not accidental. view-wiki-page now names a class of ;;;; components. The (call 'view-wiki-page ...) form in our view.ucw ;;;; entry-point is little more than a call to make-instance. ;;;; view-wiki-page, since it's a window-component, is designed to ;;;; occupy the entire browser window, it has to worry about emiting ;;;; and tags and setting up javascript includes and ;;;; style sheet links. Since view-wiki-page is also a template ;;;; component it depends an a TAL file ("wiki/view.tal") to ;;;; specify what html to output. ;;;; ** edit.ucw ;;;; *** The entry-point ;;;; Editing pages is only slightly more complicated than viewing: (defentry-point "edit.ucw" (:application *example-application*) ((page-name "WelcomePage") name summary contents) (if contents (progn (update-wiki-page page-name (make-instance 'wiki-edit :author name :summary summary :contents contents)) (call 'thankyou :page-name page-name)) (call 'edit-wiki-page :page-name page-name))) ;;;; We assume that if the request contains the contents parameter ;;;; then we're submitting an edit, otherwise we're asking for the ;;;; edit page form. We've already seen DEFENTRY-POINT, entry-point ;;;; lambda lists and call, so we can jump directly to the ;;;; edit-wiki-page component: ;;;; *** The component (defcomponent edit-wiki-page (tal-component) ((page-name :accessor page-name :initarg :page-name)) (:default-initargs :template-name "wiki/edit.tal")) ;;;; Like view-wiki-page this is also a window component based on a ;;;; TAL template. ;;;; ** The thankyou page ;;;; Just for fun we're going to use YACLML as opposed to TAL for the ;;;; thankyou component: (defcomponent thankyou () ((page-name :accessor page-name :initarg :page-name))) (defmethod render ((page thankyou)) (symbol-macrolet ((page-name (<:as-html (page-name page)))) (<:html (<:head (<:title "Thank you for editing " page-name)) (<:body (<:p "Thank you for editing " page-name) (<:a :href (strcat "view.ucw?page-name=" (page-name page)) "View " page-name))))) ;;;; As you can see by the strcat UCW isn't well adapted to munging ;;;; strings into urls. [the situation is slightly better in tal pages ;;;; with tal expression language]. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; * UCW Style ;;;; UCW styled apps, unlike regular style apps, start with components ;;;; and actions. Only later do we tie components and actions to urls ;;;; and pages. ;;;; ** wiki-manipulator ;;;; All of our wiki components are going to subclass ;;;; wiki-manipulator. This class provides the page-name slot and the ;;;; method on update-url. (defcomponent wiki-manipulator () ((page-name :accessor page-name :initarg :page-name :backtrack t))) ;;;; We define a method on update-url so that bookmarking this page, ;;;; or requesting it any time after the session has expired, will ;;;; view the current page (even if the user was editing it when he ;;;; created the bookmark). Note that there is nothing automatic about ;;;; this update-url method. It only works because we know that ;;;; wiki.ucw?page-name=Foo will show the page named Foo. Embedding ;;;; the wiki in another application would probably make this ;;;; assumption untrue and require a different update-url method. (defmethod update-url ((component wiki-manipulator) url) (setf (ucw::uri.path url) "wiki.ucw") (push (cons "page-name" (page-name component)) (ucw::uri.query url)) url) ;;;; ** wiki-viewer ;;;; The wiki-viewer component shows a page of the wiki. In particular ;;;; the component show the html version of the page named by the ;;;; value of the viewer's page-name slot. Unlike the wiki-editor ;;;; component a user will use the same wiki-viewer component during ;;;; the entire course of their browsing. (defcomponent wiki-viewer (wiki-manipulator) ()) ;;;; We split the text of the page into StudlyWords and non ;;;; StudlyWords. StudlyWords are wrapped in links to view-page ;;;; actions, everything else is sent as is to the client. (defmethod render ((page wiki-viewer)) (<:h1 (<:as-html (page-name page))) (let ((scanner (cl-ppcre:create-scanner "((?:[A-Z][a-z]+){2,})"))) (dolist (part (cl-ppcre:split scanner (contents (find-wiki-page (page-name page))) :with-registers-p t)) (if (cl-ppcre:scan scanner part) (let ((part part)) (