All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.liftweb.proto.Crudify.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2006-2011 WorldWide Conferencing, LLC
 *
 * 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.liftweb
package proto

import sitemap._
import Loc._
import http._
import util._
import common._
import Helpers._

import scala.xml._

/**
 * This trait automatically adds CRUD (Create, read, update and delete) operations
 * to an existing persistence mechanism.
 * Various methods can be overridden to
 * customize which operations are available to a user and how things are displayed.
 * For example, you can disable deletion of entities by overriding deleteMenuLoc to Empty.
 *
 */
trait Crudify {
  /**
   * The type of records we're manipulating
   */
  type TheCrudType

  /**
   * A generic representation of a field.  For example, this represents the
   * abstract "name" field and is used along with an instance of TheCrudType
   * to compute the BaseField that is the "name" field on the specific instance
   * of TheCrudType
   */
  type FieldPointerType

  /**
   * This trait represents a Bridge between TheCrudType
   * and the Crudify trait.  It's not necessary to mix this
   * trait into TheCrudType, but instead provide a mechanism
   * for promoting a TheCrudType to CrudBridge
   */
  protected trait CrudBridge {
    /**
     * Delete the instance of TheCrudType from the backing store
     */
    def delete_! : Boolean

    /**
     * Save an instance of TheCrudType in backing store
     */
    def save : Boolean

    /**
     * Validate the fields in TheCrudType and return a List[FieldError]
     * representing the errors.
     */
    def validate: List[FieldError]

    /**
     * Return a string representation of the primary key field
     */
    def primaryKeyFieldAsString: String
  }

  /**
   * This method will instantiate a bridge from TheCrudType so
   * that the appropriate logical operations can be performed
   * on TheCrudType
   */
  protected implicit def buildBridge(from: TheCrudType): CrudBridge

  protected trait FieldPointerBridge {
    /**
     * What is the display name of this field?
     */
    def displayHtml: NodeSeq
  }

  /**
   * Based on a FieldPointer, build a FieldPointerBridge
   */
  protected implicit def buildFieldBridge(from: FieldPointerType): FieldPointerBridge
  
  lazy val Prefix = calcPrefix
  lazy val ListItems = calcListItems
  lazy val ViewItem = calcViewItem
  lazy val CreateItem = calcCreateItem
  lazy val EditItem = calcEditItem
  lazy val DeleteItem = calcDeleteItem

  /**
   * What's the prefix for this CRUD.  Typically the table name.
   */
  def calcPrefix: List[String]

  /**
   * Vend a new instance of TheCrudType
   */
  def create: TheCrudType

  def calcListItems = "list"

  def calcViewItem = "view"

  def calcCreateItem = "create"

  def calcEditItem = "edit"

  def calcDeleteItem = "delete"

  def displayName = displayHtml.text

  def displayHtml: NodeSeq = Text(calcPrefix.head)

  /**
  * The fields displayed on the list page.  By default all
  * the displayed fields, but this list
  * can be shortened.
  */
  def fieldsForList: List[FieldPointerType] = fieldsForDisplay

  /**
   * When displaying a record, what fields do we display
   */
  def fieldsForDisplay: List[FieldPointerType]

  /**
   * The list of fields to present on a form form editing
   */
  def fieldsForEditing: List[FieldPointerType] = fieldsForDisplay

  def pageWrapper(body: NodeSeq): NodeSeq =
  
    {
      body
    }
  

  /**
   * The menu item for listing items (make this "Empty" to disable)
   */
  def showAllMenuLoc: Box[Menu] =
  Full(Menu(Loc("List "+Prefix, listPath, showAllMenuName,
                addlMenuLocParams ::: (
                  locSnippets :: Loc.Template(showAllTemplate) :: 
                  showAllMenuLocParams))))

  /**
   * Override to include new Params for the show all menu
   */
  def showAllMenuLocParams: List[Loc.AnyLocParam] = Nil

  /**
   * The menu item for creating items (make this "Empty" to disable)
   */
  def createMenuLoc: Box[Menu] =
  Full(Menu(Loc("Create "+Prefix, createPath, createMenuName,
                (addlMenuLocParams ::: (
                  locSnippets :: Loc.Template(createTemplate) ::
                  createMenuLocParams)))))
  /**
   * Override to include new Params for the create menu
   */
  def createMenuLocParams: List[Loc.AnyLocParam] = Nil

  /**
   * If there are any Loc.LocParams that need to be
   * added to every menu (e.g., a guard for access control
   * of the Crudify screens)
   */
  protected def addlMenuLocParams: List[Loc.AnyLocParam] = Nil


  /**
   * Customize the display of a row for displayRecord
   */
  protected def doDisplayRecordRow(entry: TheCrudType): (NodeSeq)=>NodeSeq = {
    "^" #> {
      for {
        pointer <- fieldsForDisplay
        field <- computeFieldFromPointer(entry, pointer).toList
        if field.shouldDisplay_?
      } yield {
        ".name *" #> field.displayHtml &
        ".value *" #> field.asHtml
      }
    }
  }
  
  /**
   * Customize the display of records for view menu loc
   */
  protected def displayRecord(entry: TheCrudType): (NodeSeq)=>NodeSeq = {
    ".row" #> doDisplayRecordRow(entry)
  }


  /**
   * The menu item for viewing an item (make this "Empty" to disable)
   */
  def viewMenuLoc: Box[Menu] =
  Full(Menu(new Loc[TheCrudType]{
        // the name of the page
        def name = "View "+Prefix

        override val snippets: SnippetTest = {
          case ("crud.view", Full(wp)) => displayRecord(wp.asInstanceOf[TheCrudType])
        }

        def defaultValue = Empty

        lazy val params = addlMenuLocParams ::: viewMenuLocParams

        /**
         * What's the text of the link?
         */
        val text = new Loc.LinkText(calcLinkText _)

        def calcLinkText(in: TheCrudType): NodeSeq = Text(S.?("crudify.menu.view.displayName", displayName))

        /**
         * Rewrite the request and emit the type-safe parameter
         */
        override val rewrite: LocRewrite =
        Full(NamedPF(name) {
            case RewriteRequest(pp , _, _) if hasParamFor(pp, viewPath) =>
              (RewriteResponse(viewPath), findForParam(pp.wholePath.last))
          })

        override def calcTemplate = Full(viewTemplate)

        val link =
        new Loc.Link[TheCrudType](viewPath, false) {
          override def createLink(in: TheCrudType) =
          Full(Text(viewPathString+"/"+obscurePrimaryKey(in)))
        }
      }))
  /**
   * Override to include new Params for the view menu
   */
  def viewMenuLocParams: List[Loc.LocParam[TheCrudType]] = Nil


  /**
   * The menu item for editing an item (make this "Empty" to disable)
   */
  def editMenuLoc: Box[Menu] = {
    Full(Menu(new Loc[TheCrudType]{
          // the name of the page
          def name = "Edit "+Prefix

          override val snippets: SnippetTest = {
            case ("crud.edit", Full(wp)) => crudDoForm(wp.asInstanceOf[TheCrudType], S.?("Save"))
          }

          def defaultValue = Empty

          lazy val params = addlMenuLocParams ::: editMenuLocParams

          /**
           * What's the text of the link?
           */
          val text = new Loc.LinkText(calcLinkText _)

          def calcLinkText(in: TheCrudType): NodeSeq = Text(S.?("crudify.menu.edit.displayName", displayName))

          /**
           * Rewrite the request and emit the type-safe parameter
           */
          override val rewrite: LocRewrite =
          Full(NamedPF(name) {
              case RewriteRequest(pp , _, _) if hasParamFor(pp, editPath) =>
                (RewriteResponse(editPath), findForParam(pp.wholePath.last))
            })

          override def calcTemplate = Full(editTemplate)

          val link =
          new Loc.Link[TheCrudType](editPath, false) {
            override def createLink(in: TheCrudType) =
            Full(Text(editPathString+"/"+obscurePrimaryKey(in)))
          }
        }))
  }

  /**
   * Override to include new Params for the edit menu
   */
  def editMenuLocParams: List[Loc.LocParam[TheCrudType]] = Nil


  /**
   * The String displayed for menu editing
   */
  def editMenuName = S.?("Edit")+" "+displayName

  /**
   * This is the template that's used to render the page after the
   * optional wrapping of the template in the page wrapper
   */
  def editTemplate(): NodeSeq = pageWrapper(_editTemplate)

  def editId = "edit_page"
  def editClass = "edit_class"
  def editErrorClass = "edit_error_class"

  /**
   * The core template for editing.  Does not include any
   * page wrapping.
   */
  protected def _editTemplate = {
    
 
} def editButton = S.?("Save") /** * Override this method to change how fields are displayed for delete */ protected def doDeleteFields(item: TheCrudType): (NodeSeq)=>NodeSeq = { "^" #> { for { pointer <- fieldsForDisplay field <- computeFieldFromPointer(item, pointer).toList if field.shouldDisplay_? } yield { ".name *" #> field.displayHtml & ".value *" #> field.asHtml } } } /** * Override this method to change the behavior of deleting an item */ protected def doDeleteSubmit(item: TheCrudType, from: String)() = { S.notice(S ? "Deleted") item.delete_! S.redirectTo(from) } /** * Override this method to change how the delete screen is built */ protected def crudyDelete(item: TheCrudType): (NodeSeq)=>NodeSeq = { val from = referer ".field" #> doDeleteFields(item) & "type=submit" #> SHtml.onSubmitUnit(doDeleteSubmit(item, from) _) } /** * The menu item for deleting an item (make this "Empty" to disable) */ def deleteMenuLoc: Box[Menu] = { Full(Menu(new Loc[TheCrudType]{ // the name of the page def name = "Delete "+Prefix override val snippets: SnippetTest = { case ("crud.delete", Full(wp)) => crudyDelete(wp.asInstanceOf[TheCrudType]) } def defaultValue = Empty lazy val params = addlMenuLocParams ::: deleteMenuLocParams /** * What's the text of the link? */ val text = new Loc.LinkText(calcLinkText _) def calcLinkText(in: TheCrudType): NodeSeq = Text(S.?("crudify.menu.delete.displayName", displayName)) /** * Rewrite the request and emit the type-safe parameter */ override val rewrite: LocRewrite = Full(NamedPF(name) { case RewriteRequest(pp , _, _) if hasParamFor(pp, deletePath) => (RewriteResponse(deletePath), findForParam(pp.wholePath.last)) }) override def calcTemplate = Full(deleteTemplate) val link = new Loc.Link[TheCrudType](deletePath, false) { override def createLink(in: TheCrudType) = Full(Text(deletePathString+"/"+obscurePrimaryKey(in))) } })) } private def hasParamFor(pp: ParsePath, toTest: List[String]): Boolean = { pp.wholePath.startsWith(toTest) && pp.wholePath.length == (toTest.length + 1) && findForParam(pp.wholePath.last).isDefined } /** * Override to include new Params for the delete menu */ def deleteMenuLocParams: List[Loc.LocParam[TheCrudType]] = Nil def deleteMenuName = S.?("Delete")+" "+displayName /** * This is the template that's used to render the page after the * optional wrapping of the template in the page wrapper */ def deleteTemplate(): NodeSeq = pageWrapper(_deleteTemplate) def deleteId = "delete_page" def deleteClass = "delete_class" /** * The core template for deleting. Does not include any * page wrapping. */ def _deleteTemplate = {
 
} def deleteButton = S.?("Delete") def createMenuName = S.?("Create")+" "+displayName /** * This is the template that's used to render the page after the * optional wrapping of the template in the page wrapper. */ def createTemplate(): NodeSeq = pageWrapper(_createTemplate) def createId = "create_page" def createClass = "create_class" /** * The core template for creating. Does not include any * page wrapping. */ def _createTemplate = {
 
} def createButton = S.?("Create") def viewMenuName = S.?("View")+" "+displayName /** * This is the template that's used to render the page after the * optional wrapping of the template in the page wrapper */ def viewTemplate(): NodeSeq = pageWrapper(_viewTemplate) def viewId = "view_page" def viewClass = "view_class" /** * The core template for viewing. Does not include any * page wrapping. */ def _viewTemplate = {
} def showAllMenuName = S.?("List", displayName) /** * This is the template that's used to render the page after the * optional wrapping of the template in the page wrapper */ def showAllTemplate(): NodeSeq = pageWrapper(_showAllTemplate) def showAllId = "show_all" def showAllClass = "show_all" /** * The core template for showing record. Does not include any * page wrapping */ def _showAllTemplate = {
     
{S ? "View"} {S ? "Edit"} {S ? "Delete"}
{previousWord} {nextWord}
} def nextWord = S.?("Next") def previousWord = S.?("Previous") lazy val listPath = Prefix ::: List(ListItems) lazy val listPathString: String = mp(listPath) lazy val createPath = Prefix ::: List(CreateItem) lazy val createPathString: String = mp(createPath) lazy val viewPath = Prefix ::: List(ViewItem) lazy val viewPathString: String = mp(viewPath) lazy val editPath = Prefix ::: List(EditItem) lazy val editPathString: String = mp(editPath) lazy val deletePath = Prefix ::: List(DeleteItem) lazy val deletePathString: String = mp(deletePath) private def mp(in: List[String]) = in.mkString("/", "/", "") def menus: List[Menu] = List(showAllMenuLoc, createMenuLoc, viewMenuLoc, editMenuLoc, deleteMenuLoc).flatMap(x => x) /** * Given a range, find the records. Your implementation of this * method should enforce ordering (e.g., on primary key). */ def findForList(start: Long, count: Int): List[TheCrudType] /** * Given a String that represents the primary key, find an instance of * TheCrudType */ def findForParam(in: String): Box[TheCrudType] /** * Given an instance of TheCrudType and FieldPointerType, convert * that to an actual instance of a BaseField on the instance of TheCrudType */ protected def computeFieldFromPointer(instance: TheCrudType, pointer: FieldPointerType): Box[BaseField] /** * This method defines how many rows are displayed per page. By * default, it's hard coded at 20, but you can make it session specific * or change the default by overriding this method. */ protected def rowsPerPage: Int = 20 /** * Override this method to customize how header items are treated */ protected def doCrudAllHeaderItems: (NodeSeq)=>NodeSeq = { "^ *" #> fieldsForList.map(_.displayHtml) } /** * Override this method to customize how a crudAll line is generated */ protected def doCrudAllRowItem(c: TheCrudType): (NodeSeq)=>NodeSeq = { "^" #> { for { pointer <- fieldsForList field <- computeFieldFromPointer(c, pointer).toList } yield { ".row-item *" #> field.asHtml } } } /** * Override this method to determine how all the rows on a crud * page are displayed */ protected def doCrudAllRows(list: List[TheCrudType]): (NodeSeq)=>NodeSeq = { "^" #> list.take(rowsPerPage).map { rowItem => ".row-item" #> doCrudAllRowItem(rowItem) & ".view [href]" #> (s"$viewPathString/${obscurePrimaryKey(rowItem)}") & ".edit [href]" #> (s"$editPathString/${obscurePrimaryKey(rowItem)}") & ".delete [href]" #> (s"$deletePathString/${obscurePrimaryKey(rowItem)}") } } /** * Override this method to change how the previous link is * generated */ protected def crudAllPrev(first: Long): (NodeSeq)=>NodeSeq = { if (first < rowsPerPage) { ClearNodes } else { "^ <*>" #> " #> } } /** * Override this method if you want to change the behavior * of displaying records via the crud.all snippet */ protected def doCrudAll: (NodeSeq)=>NodeSeq = { val first = S.param("first").map(toLong) openOr 0L val list = findForList(first, rowsPerPage) ".header-item" #> doCrudAllHeaderItems & ".row" #> doCrudAllRows(list) & ".previous" #> crudAllPrev(first) & ".next" #> crudAllNext(first, list) } lazy val locSnippets = new DispatchLocSnippets { val dispatch: PartialFunction[String, NodeSeq => NodeSeq] = { case "crud.all" => doCrudAll case "crud.create" => crudDoForm(create, S.?("Created")) } } /** * This method can be used to obscure the primary key. This is more secure * because end users will not have access to the primary key. */ def obscurePrimaryKey(in: TheCrudType): String = obscurePrimaryKey(in.primaryKeyFieldAsString) /** * This method can be used to obscure the primary key. This is more secure * because end users will not have access to the primary key. This method * actually does the obfuscation. You can use Mapper's KeyObfuscator class * to implement a nice implementation of this method for session-by-session * obfuscation.

* * By default, there's no obfuscation. Note that if you obfuscate the * primary key, you need to update the findForParam method to accept * the obfuscated keys (and translate them back.) */ def obscurePrimaryKey(in: String): String = in def referer: String = S.referer openOr listPathString /** * As the field names are being displayed for editing, this method * is called with the XHTML that will be displayed as the field name * and a flag indicating whether the field is required. You * can wrap the fieldName in a span with a css class indicating that * the field is required or otherwise do something to update the field * name indicating to the user that the field is required. By default * the method wraps the fieldName in a span with the class attribute set * to "required_field". */ def wrapNameInRequired(fieldName: NodeSeq, required: Boolean): NodeSeq = { if (required) { {fieldName} } else { fieldName } } def crudDoForm(item: TheCrudType, noticeMsg: String)(in: NodeSeq): NodeSeq = { val from = referer val snipName = S.currentSnippet def loop(html:NodeSeq): NodeSeq = { def error(field: BaseField): NodeSeq = { field.uniqueFieldId match { case fid @ Full(id) => S.getNotices.filter(_._3 == fid).flatMap(err => List(Text(" "), {err._2}) ) case _ => NodeSeq.Empty } } def doFields(html: NodeSeq): NodeSeq = for { pointer <- fieldsForEditing field <- computeFieldFromPointer(item, pointer).toList if field.show_? form <- field.toForm.toList bindNode = ".name *" #> { wrapNameInRequired(field.displayHtml, field.required_?) ++ error(field) } & ".form *" #> form node <- bindNode(html) } yield node def doSubmit() = item.validate match { case Nil => S.notice(noticeMsg) item.save S.redirectTo(from) case xs => S.error(xs) snipName.foreach(S.mapSnippet(_, loop)) } val bind = ".field" #> doFields _ & "type=submit" #> SHtml.onSubmitUnit(doSubmit _) bind(html) } loop(in) } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy