a3e3bd30f0d19526 Contact UI
authorTomas Zeman <tzeman@volny.cz>
Fri, 10 Feb 2012 09:53:09 +0100
changeset 15 995184977e9b
parent 14 5a2f996a5ba0
child 16 7be37d58997c
a3e3bd30f0d19526 Contact UI
src/main/scala/bootstrap/liftweb/Boot.scala
src/main/scala/fis/base/model/BaseSchema.scala
src/main/scala/fis/base/model/Entity.scala
src/main/scala/fis/crm/model/Contact.scala
src/main/scala/fis/crm/ui/ContactSnippet.scala
src/main/scala/net/tz/lift/model/OptionalFieldDisplay.scala
src/main/scala/net/tz/lift/snippet/DataTable.scala
src/main/scala/net/tz/lift/snippet/Loc.scala
src/main/scala/net/tz/lift/snippet/Panel.scala
src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala
src/main/webapp/entity/form.html
src/main/webapp/entity/list.html
src/main/webapp/entity/view.html
src/main/webapp/templates-hidden/default.html
src/main/webapp/templates-hidden/panel.html
src/main/webapp/templates-hidden/wizard-all.html
--- a/src/main/scala/bootstrap/liftweb/Boot.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -16,9 +16,11 @@
 package bootstrap.liftweb
 
 import fis.base.model._
+import fis.crm.ui.ContactSnippet
 import net.liftweb.common._
 import net.liftweb.db.{DB, ConnectionIdentifier}
 import net.liftweb.http._
+import net.liftweb.sitemap._
 import net.liftweb.squerylrecord.SquerylRecord
 import net.liftweb.util._
 import net.liftweb.util.Helpers._
@@ -39,6 +41,12 @@
 
     super.boot
 
+    import Loc._
+
+    val menus = List(Menu("/", "FIS Main page") / "index" >> Hidden,
+      Menu.i("Home") / "" , ContactSnippet.menu)
+
+    LiftRules.setSiteMap(SiteMap(menus:_*))
   }
 }
 
--- a/src/main/scala/fis/base/model/BaseSchema.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/fis/base/model/BaseSchema.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -32,6 +32,8 @@
   override def tableNameFromClass(c: Class[_]): String =
     snakify(c.getSimpleName)
 
+  override def columnNameFromPropertyName(n: String): String = snakify(n)
+
   protected def entityTable[T <: Entity[_]](name: String)(implicit manifestT:
     Manifest[T]): Table[T] = {
 
--- a/src/main/scala/fis/base/model/Entity.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/fis/base/model/Entity.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -18,6 +18,7 @@
 import net.liftweb.record.{MetaRecord, Record}
 import net.liftweb.record.field._
 import net.liftweb.squerylrecord.KeyedRecord
+import net.tz.lift.model.OptionalFieldDisplay
 import org.squeryl.annotations.Column
 
 /**
@@ -28,6 +29,7 @@
   val idField = new LongField(this.asInstanceOf[OwnerType])
   val name = new StringField(this.asInstanceOf[OwnerType], "")
   val note = new OptionalTextareaField(this.asInstanceOf[OwnerType], 10240)
+    with OptionalFieldDisplay
 
   /** Display representation of this entity. */
   def linkName = name.get
--- a/src/main/scala/fis/crm/model/Contact.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/fis/crm/model/Contact.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -16,8 +16,25 @@
 package fis.crm.model
 
 import fis.base.model.{Entity, EntityUnion, MetaEntity}
+import net.liftweb.common._
+import net.liftweb.http.S
 import net.liftweb.record.{MetaRecord, Record}
 import net.liftweb.record.field._
+import net.liftweb.util.FieldError
+import net.tz.lift.model.OptionalFieldDisplay
+import scala.xml.Text
+
+class OptionalEmailField[OwnerType <: Record[OwnerType]](rec: OwnerType,
+  maxLength: Int) extends OptionalStringField[OwnerType](rec, maxLength) {
+  override def validations = validateEmail _ :: Nil
+  protected def validateEmail(email: ValueType): List[FieldError] =
+    toBoxMyType(email) match {
+      case Empty | Full("") => Nil
+      case Full(v) if EmailField.validEmailAddr_?(v) => Nil
+      case _ => Text(S.??("invalid.email.address"))
+    }
+}
+
 
 class Contact private() extends Record[Contact] with Entity[Contact] {
 
@@ -25,26 +42,41 @@
 
   val firstName = new StringField(this, 80, "")
   val lastName = new StringField(this, 80, "")
-  val position = new OptionalStringField(this, 40)
+  val position = new OptionalStringField(this, 40) with OptionalFieldDisplay
   val workMail = new EmailField(this, 256)
-  val privateMail = new OptionalEmailField(this, 256)
-  val otherMail = new OptionalEmailField(this, 256)
+  val privateMail = new OptionalEmailField(this, 256) with OptionalFieldDisplay
+  val otherMail = new OptionalEmailField(this, 256) with OptionalFieldDisplay
   val workMobile = new StringField(this, 40)
-  val privateMobile = new OptionalStringField(this, 40)
-  val otherMobile = new OptionalStringField(this, 40)
-  val workPhone = new OptionalStringField(this, 40)
-  val privatePhone = new OptionalStringField(this, 40)
-  val fax = new OptionalStringField(this, 40)
+  val privateMobile = new OptionalStringField(this, 40) with
+    OptionalFieldDisplay
+  val otherMobile = new OptionalStringField(this, 40) with OptionalFieldDisplay
+  val workPhone = new OptionalStringField(this, 40) with OptionalFieldDisplay
+  val privatePhone = new OptionalStringField(this, 40) with OptionalFieldDisplay
+  val fax = new OptionalStringField(this, 40) with OptionalFieldDisplay
 
   override def linkName = lastName.get + " " + firstName.get
 
   lazy val entities = CrmSchema.entityContacts.right(this)
+
+  def fieldsForView = List(lastName, firstName, position, workMail, workMobile,
+    workPhone, privateMail, privateMobile, privatePhone, fax,
+    otherMail, otherMobile, note)
 }
 
+import net.liftweb.squerylrecord.RecordTypeMode._
+
 object Contact extends Contact with MetaRecord[Contact] with
   MetaEntity[Contact] {
 
   def getTable = CrmSchema.contacts
+  def contacts: List[Contact] = from(getTable) (c =>
+    select(c) orderBy(c.lastName asc, c.firstName asc)
+  ) toList
+
+  def fieldsForList = List(position, workMail,
+    workMobile, note)
+
+  def delete(c: Contact) = getTable.delete(c.id)
 }
 
 trait ContactsAware { self: Entity[_] =>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/crm/ui/ContactSnippet.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package fis.crm.ui
+
+import fis.crm.model._
+import net.liftweb.common._
+import net.liftweb.http._
+import net.liftweb.sitemap._
+import net.liftweb.util._
+import net.liftweb.util.Helpers._
+import net.tz.lift.snippet.{A, DataTable, LocTpl, TplPanel, SnippetHelpers}
+import scala.xml.{Elem, NodeSeq, Text}
+
+sealed trait ContactLoc {
+  def title: String
+  def url: List[String]
+  def tpl: String
+}
+
+case object ListContacts extends ContactLoc {
+  def title = "Contacts"
+  def url = Nil
+  def tpl = "entity/list"
+}
+
+case object AddContact extends ContactLoc {
+  def title = "Add contact"
+  def url = List("add")
+  def tpl = "entity/form"
+}
+
+case class ShowContact(c: Contact) extends ContactLoc {
+  def title = "Contact " + c.linkName
+  def url = List(c.id.toString)
+  def tpl = "entity/view"
+}
+
+case class EditContact(c: Contact) extends ContactLoc {
+  def title = "Edit contact " + c.linkName
+  def url = List(c.id.toString, "edit")
+  def tpl = "entity/form"
+}
+
+case class DeleteContact(c: Contact) extends ContactLoc {
+  def title = "Delete contact " + c.linkName
+  def url = List(c.id.toString, "delete")
+  def tpl = "entity/form"
+}
+
+object AsContactLoc extends Loggable {
+  def apply(pars: List[String]): Box[ContactLoc] = {
+    logger.debug("Params: " + pars)
+    pars match {
+    case List("add") => Full(AddContact)
+    case AsLong(id) :: xs => Contact.findByKey(id) map { c => xs match {
+      case List("edit") => EditContact(c)
+      case List("delete") => DeleteContact(c)
+      case _ => ShowContact(c)
+    }}
+    case Nil => Full(ListContacts)
+    case _ => Empty
+  }
+  }
+}
+
+object ContactSnippet extends SnippetHelpers {
+  import Loc._
+
+  val prefix = "contact"
+
+  lazy val menu: Menu = Menu("contact.list", ListContacts.title) / "contacts" >>
+    EarlyResponse(() => Full(RedirectResponse(url(ListContacts)))) submenus(
+      Menu("contact.add", AddContact.title) / prefix / "add",
+      Menu.params[ContactLoc]("contact.edit", "Edit contact", parMenu.parser,
+        parMenu.encoder) / prefix / * / "edit" >> CalcValue(() => cur.is match {
+          case ShowContact(c) => Full(ShowContact(c))
+          case _ => Empty
+        }) ,
+      Menu.params[ContactLoc]("contact.delete", "Delete contact", parMenu.parser,
+        parMenu.encoder) / prefix / * / "delete" >> CalcValue(() => cur.is match {
+          case ShowContact(c) => Full(ShowContact(c))
+          case _ => Empty
+        }) ,
+      menu1)
+
+  lazy val menu1: Menu = parMenu >> new DispatchLocSnippets {
+      def dispatch = { n => (n, cur.is) match {
+        case ("list", ListContacts) => listContacts
+        case ("panel", ShowContact(c)) => ContactPanel(c)
+        case ("form", EditContact(c)) => ContactForm(c).dispatch("")
+        case ("form", AddContact) => ContactForm(Contact.createRecord).
+          dispatch("")
+        case ("form", DeleteContact(c)) => DeleteContactForm(c).dispatch("")
+      }}
+    } >> LocTpl(_.tpl) >> Hidden
+
+  lazy val parMenu = Menu.params[ContactLoc]("contact",
+    Loc.LinkText(l => Text(l.title)),
+    AsContactLoc(_) pass { _.foreach { cur(_) } }, _.url) / prefix / **
+
+  def url(l: ContactLoc) = parMenu.toLoc.calcHref(l)
+
+  object cur extends RequestVar[ContactLoc](ListContacts)
+
+  def listContacts: CssTr = {
+    val cols = Contact.fieldsForList.map { _.displayName }
+    val cells = Contact.contacts.map { c =>
+      A(url(ShowContact(c)), Text(c.linkName)) ::
+      (Contact.fieldsForList.map { f =>
+      c.fieldByName(f.name).map { _.asHtml } openOr NodeSeq.Empty
+    }.toList) }.toList
+    new DataTable((S ? "Full name") :: cols, cells)
+  }
+
+  case class ContactPanel(c: Contact) extends TplPanel(c.fieldsForView.map
+    { f => (f.displayHtml, f.asHtml) })
+
+  case class ContactForm(c: Contact) extends LiftScreen {
+    object contact extends ScreenVar(c)
+    contact.is.fieldsForView.map { f => addFields(() => f) }
+    override def finishButton: Elem = <button>{S ? "Save"}</button>
+    protected def finish() {
+      val c = contact.is
+      Contact.getTable.insertOrUpdate(c)
+      S.notice("Contact " + c.linkName + " saved")
+      S.redirectTo(url(ShowContact(c)))
+      ()
+    }
+  }
+
+  case class DeleteContactForm(c: Contact) extends LiftScreen {
+    val confirm = field("Really delete this contact?", false)
+    override def finishButton: Elem = <button>{S ? "Delete"}</button>
+    protected def finish() {
+      confirm.is match {
+        case true =>
+          Contact.delete(c)
+          S.notice("Contact " + c.linkName + " deleted")
+          S.redirectTo(url(ListContacts))
+        case false =>
+          S.redirectTo(url(ShowContact(c)))
+      }
+      ()
+    }
+  }
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/model/OptionalFieldDisplay.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.tz.lift.model
+
+import net.liftweb.record.OptionalTypedField
+import scala.xml.Text
+
+/**
+ * Nice display of optional fields, ie. w/o Some(...)
+ */
+trait OptionalFieldDisplay { self: OptionalTypedField[_] =>
+  override def asHtml = Text(get map { _.toString } getOrElse "")
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/snippet/DataTable.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.tz.lift.snippet
+
+import net.liftweb.http.TemplateFinder
+import net.liftweb.util.Helpers._ // CSS transforms
+import scala.xml.NodeSeq
+
+/**
+ * Template driven table, compatible w/ datatables.net.
+ * Template location: <code>/templates-hidden/datatable</code>.
+ */
+class DataTable(cols: List[String], cells: List[List[NodeSeq]]) extends CssTr {
+  def apply(in: NodeSeq): NodeSeq = TemplateFinder.findAnyTemplate(
+    List("templates-hidden", "datatable")) map { xml =>
+    (".field-name *" #> cols &
+    ".row *" #> cells.map { vals => "td *" #> vals })(xml)
+  } openOr NodeSeq.Empty
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/snippet/Loc.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.tz.lift.snippet
+
+import net.liftweb.http.TemplateFinder
+import net.liftweb.sitemap.Loc._
+import scala.xml.NodeSeq
+
+/**
+ * Helper object for custom Loc template based on template path, eg.
+ * <code>item/view</code>.
+ */
+object LocTpl {
+
+  def apply(path: String): Template = Template(() =>
+    TemplateFinder.findAnyTemplate(path split "/" toList) openOr NodeSeq.Empty)
+
+  def apply[T](f: T => String): ValueTemplate[T] = ValueTemplate[T](
+    _.flatMap { v => TemplateFinder.findAnyTemplate(f(v) split "/" toList)
+    } openOr NodeSeq.Empty)
+}
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/scala/net/tz/lift/snippet/Panel.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/net/tz/lift/snippet/Panel.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -46,12 +46,28 @@
     new Panel(fields.map(AttrRow(_)))
 }
 
-class Panel(attrs: => Iterable[AttrRow]) extends Function1[NodeSeq, NodeSeq]
-{
+class Panel(attrs: => Iterable[AttrRow]) extends CssTr {
   def apply(in: NodeSeq): NodeSeq = <table>{attrs.map(_())}</table>
   def &(other: Function1[NodeSeq, NodeSeq]) = new Function1[NodeSeq, NodeSeq] {
     def apply(in: NodeSeq): NodeSeq = List(Panel.this, other) flatMap (_(in))
   }
 }
 
+import net.liftweb.http.TemplateFinder
+import net.liftweb.util.Helpers._ // CSS transforms
+
+/**
+ * A panel using template in <code>/templates-hidden/panel</code> for
+ * values rendering.
+ */
+class TplPanel(cells: List[(NodeSeq, NodeSeq)]) extends CssTr {
+  def apply(in: NodeSeq): NodeSeq = TemplateFinder.findAnyTemplate(
+    List("templates-hidden", "panel")) map { xml =>
+    (".row *" #> cells.map { r =>
+      ".n *" #> r._1 &
+      ".v *" #> r._2
+    })(xml)
+  } openOr NodeSeq.Empty
+}
+
 // vim: set ts=2 sw=2 et:
--- a/src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala	Fri Feb 10 09:53:09 2012 +0100
@@ -19,10 +19,15 @@
 import scala.xml.{NodeSeq, Text}
 
 trait SnippetHelpers {
+
+  type CssTr = (NodeSeq => NodeSeq)
+
   def mkPath(prefix: String, l: String*) =
     (prefix :: l.toList) mkString ("/", "/", "")
 }
 
+trait CssTr extends Function1[NodeSeq, NodeSeq]
+
 class A(href: => String) extends Function1[NodeSeq, NodeSeq] {
   def apply(in: NodeSeq): NodeSeq = <a href={href}>{in}</a>
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/entity/form.html	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Entity View</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="span-24 last">
+        <h2><span class="lift:Menu.title"/></h2>
+      </div>
+      <div class="span-24 last">
+        <span class="lift:form"/>
+      </div>
+    </div>
+  </body>
+</html>
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/entity/list.html	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Entity List</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="span-24 last">
+        <h2><span class="lift:Menu.title"/></h2>
+      </div>
+      <div class="span-24 last">
+        <span class="lift:list"/>
+      </div>
+    </div>
+  </body>
+</html>
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/entity/view.html	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Entity View</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="span-24 last">
+        <h2><span class="lift:Menu.title"/></h2>
+      </div>
+      <div class="span-24 last">
+        <span class="lift:panel"/>
+      </div>
+    </div>
+  </body>
+</html>
+
+
--- a/src/main/webapp/templates-hidden/default.html	Fri Feb 10 09:53:08 2012 +0100
+++ b/src/main/webapp/templates-hidden/default.html	Fri Feb 10 09:53:09 2012 +0100
@@ -23,8 +23,8 @@
       <hr />
       <div class="column span-24 last">
         <span class="lift:Menubar"></span>
-        <div class="lift:Msgs?showAll=true"></div>
       </div>
+      <div class="lift:Msgs?showAll=false column span-24 last"></div>
       <div class="span-10 last lift:action-links.are">
         <span class="lift:action-links"/>
       </div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/templates-hidden/panel.html	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,6 @@
+<table>
+  <tr class="row">
+    <td class="n attr-name">Attribute</td>
+    <td class="v attr-value">Value</td>
+  </tr>
+</table>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/templates-hidden/wizard-all.html	Fri Feb 10 09:53:09 2012 +0100
@@ -0,0 +1,61 @@
+<div>
+  <wizard:screen_info>
+    <div>Page <wizard:screen_number/> of <wizard:total_screens/></div>
+  </wizard:screen_info>
+  <wizard:wizard_top> <div> <wizard:bind/> </div> </wizard:wizard_top>
+  <wizard:screen_top> <div> <wizard:bind/> </div> </wizard:screen_top>
+  <wizard:errors>
+    <div>
+      <ul>
+        <wizard:item><li><wizard:bind/></li></wizard:item>
+      </ul>
+    </div>
+  </wizard:errors>
+  <div>
+    <wizard:fields>
+      <table>
+        <tbody>
+          <tr lift:bind="wizard:line">
+            <td class="form-name">
+              <wizard:label>
+                <label wizard:for="">
+                  <wizard:bind></wizard:bind>
+                </label>
+              </wizard:label>
+              <wizard:help>
+                <span>
+                  <wizard:bind></wizard:bind>
+                </span>
+              </wizard:help>
+              <wizard:field_errors>
+                <ul>
+                  <wizard:error>
+                    <li><wizard:bind></wizard:bind></li>
+                  </wizard:error> 
+                </ul>
+              </wizard:field_errors>
+            </td>
+            <td class="form-value">
+              <wizard:form></wizard:form>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </wizard:fields>
+  </div>
+  <div>
+    <table>
+      <tr> 
+        <td><wizard:prev></wizard:prev></td>
+        <td><wizard:cancel></wizard:cancel></td>
+        <td><wizard:next></wizard:next></td>
+      </tr>
+    </table>
+  </div>
+  <wizard:screen_bottom>
+    <div><wizard:bind></wizard:bind></div>
+  </wizard:screen_bottom>
+  <wizard:wizard_bottom>
+    <div><wizard:bind></wizard:bind></div>
+  </wizard:wizard_bottom>
+</div>