Contact record refactoring
authorTomas Zeman <tzeman@volny.cz>
Tue, 03 Apr 2012 14:39:58 +0200
changeset 35 d77d8194ee59
parent 34 041c5c5d2bba
child 36 5ae643e27ef9
Contact record refactoring
src/main/scala/bootstrap/liftweb/Boot.scala
src/main/scala/fis/base/ui/EntitySnippet.scala
src/main/scala/fis/crm/model/Contact.scala
src/main/scala/fis/crm/model/ContactCrud.scala
src/main/scala/fis/crm/model/CrmSchema.scala
src/main/scala/fis/crm/ui/ContactPanel.scala
src/main/scala/fis/crm/ui/ContactSnippet.scala
src/main/scala/net/tz/lift/snippet/package.scala
src/test/scala/fis/crm/model/ContactSpec.scala
--- a/src/main/scala/bootstrap/liftweb/Boot.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -16,7 +16,7 @@
 package bootstrap.liftweb
 
 import fis.base.model._
-import fis.crm.ui.{ContactSnippet, ContactSnippet2}
+import fis.crm.ui.ContactSnippet
 import fis.db.SquerylTxMgr
 import net.liftweb.common._
 import net.liftweb.http._
@@ -37,7 +37,7 @@
     import Loc._
 
     val menus = List(Menu("/", "FIS Main page") / "index" >> Hidden,
-      Menu.i("Home") / "" , ContactSnippet.menu, ContactSnippet2.menu)
+      Menu.i("Home") / "" , ContactSnippet.menu)
 
     LiftRules.setSiteMap(SiteMap(menus:_*))
   }
--- a/src/main/scala/fis/base/ui/EntitySnippet.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/fis/base/ui/EntitySnippet.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -15,8 +15,10 @@
  */
 package fis.base.ui
 
+import fis.base.model.RecordCrud
 import net.liftweb.common._
 import net.liftweb.http._
+import net.liftweb.record._
 import net.liftweb.sitemap._
 import net.liftweb.util._
 import net.liftweb.util.Helpers._
@@ -85,17 +87,27 @@
   def deleteMenu = deletePreMenu >> LocTpl(tplDelete)
 }
 
-trait EntitySnippet[T <: KeyedRecord[Long]] extends SnippetHelpers with
-  CrudMenu[T] {
+trait EntitySnippet[T <: Record[T] with KeyedRecord[Long]] extends
+  RecordCrud[T] {
+
+  def prefix: String
 
-  object cur extends RequestVar[EntityLoc[T]](ListEntity[T]())
+  protected object memo extends RequestMemoize[String, Box[T]]()
+  lazy val parse = { id: String => memo(id, get(id)) }
+  lazy val encode = { v: T => v.id.toString }
 
+  val ADD = "add"
+  val EDIT = "edit"
+  val DELETE = "delete"
+
+  /*
   def listsMenu = Menu(prefix + "s", titleList) / (prefix + "s") >>
     EarlyResponse(() => Full(RedirectResponse("/" + prefix)))
 
   lazy val menu: Menu = listsMenu submenus(
     listMenu, addMenu, showMenu, editMenu, deleteMenu
   )
+  */
 }
 
 // vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/crm/model/Contact.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/fis/crm/model/Contact.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -17,73 +17,33 @@
 
 import fis.base.model.{Entity, 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 net.liftweb.squerylrecord.RecordTypeMode._
+import net.tz.lift.model.{FieldLabel => FL}
+import net.tz.lift.model.{OptionalFieldDisplay => OptDisp}
 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] {
 
   def meta = Contact
 
-  val firstName = new StringField(this, 80, "")
-  val lastName = new StringField(this, 80, "")
-  val position = new OptionalStringField(this, 40) with OptionalFieldDisplay
-  val workMail = new EmailField(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) 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
+  val firstName = new StringField(this, 80, "") with FL
+  val lastName = new StringField(this, 80, "") with FL
+  val position = new OptionalStringField(this, 40) with OptDisp with FL
+  val workMail = new EmailField(this, 256) with FL
+  val privateMail = new OptionalEmailField(this, 256) with OptDisp with FL
+  val otherMail = new OptionalEmailField(this, 256) with OptDisp with FL
+  val workMobile = new StringField(this, 40) with FL
+  val privateMobile = new OptionalStringField(this, 40) with OptDisp with FL
+  val otherMobile = new OptionalStringField(this, 40) with OptDisp with FL
+  val workPhone = new OptionalStringField(this, 40) with OptDisp with FL
+  val privatePhone = new OptionalStringField(this, 40) with OptDisp with FL
+  val fax = new OptionalStringField(this, 40) with OptDisp with FL
 
   override def linkName = lastName.get + " " + firstName.get
-
-  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)
-}
-
-import org.squeryl.KeyedEntity
-import org.squeryl.dsl.CompositeKey3
-
-case class EntityContact(val entityId: Long, val contactId: Long,
-  val typeId: Long) extends KeyedEntity[CompositeKey3[Long, Long, Long]] {
-
-  def id = CompositeKey3(entityId, contactId, typeId)
-}
+object Contact extends Contact with MetaRecord[Contact]
 
 // vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/crm/model/ContactCrud.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2012 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.model
+
+import fis.base.model.RecordCrud
+
+trait ContactCrud extends RecordCrud[Contact] {
+  val table = CrmSchema.contactT
+}
+
+object ContactCrud extends ContactCrud
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/crm/model/CrmSchema.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/fis/crm/model/CrmSchema.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -21,7 +21,11 @@
 
 trait CrmSchema extends BaseSchema with GeoSchema {
 
-  val contacts = table[Contact]("contact")
+  val contactT = tableWithSeq[Contact]
+
+  def allContacts: Iterable[Contact] = from(contactT) (c =>
+    select(c) orderBy(c.lastName asc, c.firstName asc)
+  )
 
   /*
   val entityContacts = manyToManyRelation(entities, contacts).
@@ -52,4 +56,19 @@
 
 object CrmSchema extends CrmSchema
 
+import org.squeryl.KeyedEntity
+import org.squeryl.dsl._
+
+case class EntityContact[T](val entityId: Long, val contactId: Long) extends
+  KeyedEntity[CompositeKey2[Long, Long]] {
+
+  def id = CompositeKey2(entityId, contactId)
+}
+
+case class EntityContactWithType[T](val entityId: Long, val contactId: Long,
+  val typeId: Long) extends KeyedEntity[CompositeKey3[Long, Long, Long]] {
+
+  def id = CompositeKey3(entityId, contactId, typeId)
+}
+
 // vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/crm/ui/ContactPanel.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2011-2012 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.base.ui.ViewPanel
+import fis.crm.model._
+import net.liftweb.util.BaseField
+
+object ContactPanel {
+
+  def fields(c: Contact): List[BaseField] = List(c.lastName, c.firstName,
+    c.position, c.workMail, c.workMobile, c.workPhone, c.privateMail,
+    c.privateMobile, c.privatePhone, c.fax, c.otherMail, c.otherMobile, c.note)
+
+  def apply(c: Contact) = ViewPanel(fields(c))
+}
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/crm/ui/ContactSnippet.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/fis/crm/ui/ContactSnippet.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ * Copyright 2011-2012 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.
@@ -15,160 +15,89 @@
  */
 package fis.crm.ui
 
+import fis.base.ui._
 import fis.crm.model._
 import net.liftweb.common._
 import net.liftweb.http._
 import net.liftweb.sitemap._
+import net.liftweb.sitemap.Loc._
 import net.liftweb.util._
 import net.liftweb.util.Helpers._
-import net.tz.lift.snippet.{A, CssTr, DataTable, LocTpl, TplPanel, SnippetHelpers}
+import net.tz.lift.model._
+import net.tz.lift.snippet._
 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._
-
+object ContactSnippet extends ContactCrud with EntitySnippet[Contact] {
   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)
+  val listOp = Menu("contact.list", l10n("Contacts")) / prefix >>
+    Title(_ => i18n("Contacts")) >>
+    locTpl("entity/list") >> Snippet("list", list)
+
+  val createOp = Menu("contact.create", l10n("Create")) / prefix / ADD >>
+    Title(_ => i18n("Create contact")) >>
+    locTpl("entity/form") >> Snippet("form", form) >> Hidden
+
+  val viewOp = Menu.param[Contact]("contact.view", l10n("Contact"), parse,
+    encode) / prefix / * >> Title(c => i18n("Contact %s", c.linkName)) >>
+    locTpl("entity/view") >> Snippet("panel", panel) >>
+    Hidden
+
+  val editOp = Menu.param[Contact]("contact.edit", l10n("Edit"), parse,
+    encode) / prefix / * / EDIT >>
+    Title(c => i18n("Edit contact %s", c.linkName)) >>
+    locTpl("entity/form") >> Snippet("form", form) >> Hidden
+
+  val deleteOp = Menu.param[Contact]("contact.delete", l10n("Delete"),
+    parse, encode) / prefix / * / DELETE >>
+    Title(c => i18n("Delete contact %s", c.linkName)) >>
+    locTpl("entity/delete") >> Snippet("form", deleteF) >> Hidden
 
-  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 viewLoc: Loc[Contact] = viewOp.toLoc
+  lazy val editLoc = editOp.toLoc
+  lazy val deleteLoc = deleteOp.toLoc
+
+  val menu = listOp submenus (viewOp, editOp, createOp, deleteOp)
+
+  def list: CssTr = /*ContactTable(from(CrmSchema.contacts)(c =>
+    select(c) orderBy(c.lastName asc, c.firstName asc)))*/ ClearNodes
+
+  def panel: CssTr = "*" #> viewLoc.currentValue.map { ContactPanel(_) }
 
-  lazy val parMenu = Menu.params[ContactLoc]("contact",
-    Loc.LinkText(l => Text(l.title)),
-    AsContactLoc(_) pass { _.foreach { cur(_) } }, _.url) / prefix / **
+  object form extends LiftScreen {
+    object c extends ScreenVar[Contact](Contact.createRecord)
 
-  def url(l: ContactLoc) = parMenu.toLoc.calcHref(l)
-
-  object cur extends RequestVar[ContactLoc](ListContacts)
+    override def screenFields: List[BaseField] = ContactPanel.fields(c)
 
-  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)
+    override def localSetup() {
+      editLoc.currentValue.foreach(c(_))
+    }
+
+    def finish() { save(c) foreach { v =>
+      S notice l10n("Contact %s saved", v.linkName)
+      S.redirectTo(viewLoc.calcHref(v))
+    }}
   }
 
-  case class ContactPanel(c: Contact) extends TplPanel(c.fieldsForView.map
-    { f => (f.displayHtml, f.asHtml) })
+  object deleteF extends LiftScreen {
+    val confirm = field(l10n("Really delete this contact?"), false)
+
+    override def finishButton: Elem = <button>{l10n("Delete")}</button>
 
-  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)))
-      ()
+    def finish() {
+      for {
+        c <- deleteLoc.currentValue if confirm
+        r <- delete(c)
+        n <- r.box(c.linkName)
+      } {
+        S notice l10n("Contact %s deleted", n)
+        S redirectTo listOp.loc.calcDefaultHref
+      }
     }
   }
 
-  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)))
-      }
-      ()
-    }
-  }
+
 }
 
-import fis.base.ui._
-
-object ContactSnippet2 extends EntitySnippet[Contact] {
-  implicit def str2ns(s: String): NodeSeq = Text(s)
-  def prefix = "contact2"
-  def asEntity(id: Long): Box[Contact] = Contact.findByKey(id)
-  def titleList = "Contacts"
-  def titleAdd = "Create a contact"
-  def titleShow(c: Contact) = "Contact " + c.linkName
-  def titleEdit(c: Contact) = "Edit contact " + c.linkName
-  def titleDelete(c: Contact) = "Delete contact " + c.linkName
-}
 
 // vim: set ts=2 sw=2 et:
--- a/src/main/scala/net/tz/lift/snippet/package.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/main/scala/net/tz/lift/snippet/package.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -29,6 +29,7 @@
   def a(href: Box[String])(cnt: NodeSeq): NodeSeq =
     href.dmap(cnt) { l => <a href={l}>{cnt}</a> }
 
+  def locTpl(p: String): Loc.Template = LocTpl(p)
 }
 
 
--- a/src/test/scala/fis/crm/model/ContactSpec.scala	Tue Apr 03 14:31:12 2012 +0200
+++ b/src/test/scala/fis/crm/model/ContactSpec.scala	Tue Apr 03 14:39:58 2012 +0200
@@ -22,7 +22,6 @@
 import org.scalatest._
 
 class ContactSpec extends AbstractTest with BeforeAndAfterAll {
-  import CrmSchema._
 
   override def beforeAll() {
     super.beforeAll
@@ -31,13 +30,13 @@
 
   "EntityContact" should "create" in { doInDB {
     val c1 = Contact.createRecord.name("c1")
-    contacts.insert(c1)
+    ContactCrud.save(c1) should be ('defined)
     /*
     val t1 = CodeListItem.createRecord.name("t1")
     codeListItems.insert(t1)
     */
     val c2 = Contact.createRecord.name("c2") // acts here as entity
-    contacts.insert(c2)
+    ContactCrud.save(c2) should be ('defined)
     /*
     entityContacts.insert(EntityContact(c2.id, c1.id, t1.id))