package com.pocketchangeapp.model // Import all of the mapper classes import _root_.net.liftweb.mapper._ // Create a User class extending the Mapper base class // MegaProtoUser, which provides default fields and methods // for a site user. class User extends MegaProtoUser[User] { def getSingleton = User // reference to the companion object below def allAccounts : List[Account] = Account.findAll(By(Account.owner, this.id)) } // Create a "companion object" to the User class (above). // The companion object is a "singleton" object that shares the same // name as its companion class. It provides global (i.e. non-instance) // methods and fields, such as find, dbTableName, dbIndexes, etc. // For more, see the Scala documentation on singleton objects object User extends User with MetaMegaProtoUser[User] { override def dbTableName = "users" // define the DB table name // Provide our own login page template. override def loginXhtml = <lift:surround with="default" at="content"> { super.loginXhtml } </lift:surround> // Provide our own signup page template. override def signupXhtml(user: User) = <lift:surround with="default" at="content"> { super.signupXhtml(user) } </lift:surround> }
package com.pocketchangeapp.model import _root_.java.math.MathContext import _root_.net.liftweb.mapper._ import _root_.net.liftweb.util.Empty // Create an Account class extending the LongKeyedMapper superclass // (which is a "mapped" (to the database) trait that uses a Long primary key) // and mixes in trait IdPK, which adds a primary key called "id". class Account extends LongKeyedMapper[Account] with IdPK { // Define the singleton, as in the "User" class def getSingleton = Account // Define a many-to-one (foreign key) relationship to the User class object owner extends MappedLongForeignKey(this, User) { // Change the default behavior to add a database index // for this column. override def dbIndexed_? = true } // Define an "access control" field that defaults to false. We’ll // use this in the SiteMap chapter to allow the Account owner to // share out an account view. object is_public extends MappedBoolean(this) { override def defaultValue = false } // Define the field to hold the actual account balance with up to 16 // digits (DECIMAL64) and 2 decimal places object balance extends MappedDecimal(this, MathContext.DECIMAL64, 2) object name extends MappedString(this,100) object description extends MappedString(this, 300) // Define utility methods for simplifying access to related classes. We’ll // cover how these methods work in the Mapper chapter def admins = AccountAdmin.findAll(By(AccountAdmin.account, this.id)) def addAdmin (user : User) = AccountAdmin.create.account(this).administrator(user).save def viewers = AccountViewer.findAll(By(AccountViewer.account, this.id)) def entries = Expense.getByAcct(this, Empty, Empty, Empty) def tags = Tag.findAll(By(Tag.account, this.id)) def notes = AccountNote.findAll(By(AccountNote.account, this.id)) } // The companion object to the above Class object Account extends Account with LongKeyedMetaMapper[Account] { // Define a utility method for locating an account by owner and name def findByName (owner : User, name : String) : List[Account] = Account.findAll(By(Account.owner, owner.id.is), By(Account.name, name)) ... more utility methods ... }
<lift:surround with="default" at="content"> <head> <!-- include the required plugins --> <script type="text/javascript" src="/scripts/date.js"></script> <!--[if IE]> <script type="text/javascript" src="/scripts/jquery.bgiframe.js"> </script> <![endif]--> <!-- include the jQuery DatePicker JavaScript and CSS --> <script type="text/javascript" src="/scripts/jquery.datePicker.js"> </script> <link rel="stylesheet" type="text/css" href="/style/datePicker.css" /> </head> <!-- The contents of this element will be passed to the summary method in the HomePage snippet. The call to bind in that method will replace the XML tags below (e.g. account:name) with the account data and return a NodeSeq to replace the lift:HomePage.summary element. --> <lift:HomePage.summary> <div class="column span-24 bordered"> <h2>Summary of accounts:</h2> <account:entry> <acct:name /> : <acct:balance /> <br/> </account:entry> </div> <hr /> </lift:HomePage.summary> <div class="column span-24"> <!-- The contents of this element will be passed into the add method in the AddEntry snippet. A form element with method "POST" will be created and the XML tags (e.g. e:account) below will be replaced with form elements via the call to bind in the add method. This form will replace the lift:AddEntry.addentry element below. --> <lift:AddEntry.addentry form="POST"> <div id="entryform"> <div class="column span-24"><h3>Entry Form</h3> <e:account /> <e:dateOf /> <e:desc /> <e:value /> <e:tags/><button>Add $</button> </div> </div> </lift:AddEntry.addentry> </div> <script type="text/javascript"> Date.format = ’yyyy/mm/dd’; jQuery(function () { jQuery(’#entrydate’).datePicker({startDate:’00010101’, clickInput:true}); }) </script> </lift:surround>
package com.pocketchangeapp.snippet import ... standard imports ... import _root_.com.pocketchangeapp.model._ import _root_.java.util.Date class HomePage { // User.currentUser returns a "Box" object, which is either Full // (i.e. contains a User), Failure (contains error data), or Empty. // The Scala match method is used to select an action to take based // on whether the Box is Full, or not ("case _" catches anything // not caught by "case Full(user)". See Box in the Lift API. We also // briefly discuss Box in Appendix C. def summary (xhtml : NodeSeq) : NodeSeq = User.currentUser match { case Full(user) => { val entries : NodeSeq = user.allAccounts match { case Nil => Text("You have no accounts set up") case accounts => accounts.flatMap({account => bind("acct", chooseTemplate("account", "entry", xhtml), "name" -> <a href={"/account/" + account.name.is}> {account.name.is}</a>, "balance" -> Text(account.balance.toString)) }) } bind("account", xhtml, "entry" -> entries) } case _ => <lift:embed what="welcome_msg" /> } }
package com.pocketchangeapp.snippet import ... standard imports ... import com.pocketchangeapp.model._ import com.pocketchangeapp.util.Util import java.util.Date /* date | desc | tags | value */ class AddEntry extends StatefulSnippet { // This maps the "addentry" XML element to the "add" method below def dispatch = { case "addentry" => add _ } var account : Long = _ var date = "" var desc = "" var value = "" // S.param("tag") returns a "Box" and the "openOr" method returns // either the contents of that box (if it is "Full"), or the empty // String passed to it, if the Box is "Empty". The S.param method // returns parameters passed by the browser. In this instance, the // name of the parameter is "tag". var tags = S.param("tag") openOr "" def add(in: NodeSeq): NodeSeq = User.currentUser match { case Full(user) if user.editable.size > 0 => { def doTagsAndSubmit(t: String) { tags = t if (tags.trim.length == 0) S.error("We’re going to need at least one tag.") else { // Get the date correctly, comes in as yyyy/mm/dd val entryDate = Util.slashDate.parse(date) val amount = BigDecimal(value) val currentAccount = Account.find(account).open_! // We need to determine the last serial number and balance // for the date in question. This method returns two values // which are placed in entrySerial and entryBalance // respectively val (entrySerial, entryBalance) = Expense.getLastExpenseData(currentAccount, entryDate) val e = Expense.create.account(account) .dateOf(entryDate) .serialNumber(entrySerial + 1) .description(desc) .amount(BigDecimal(value)).tags(tags) .currentBalance(entryBalance + amount) // The validate method returns Nil if there are no errors, // or an error message if errors are found. e.validate match { case Nil => { Expense.updateEntries(entrySerial + 1, amount) e.save val acct = Account.find(account).open_! val newBalance = acct.balance.is + e.amount.is acct.balance(newBalance).save S.notice("Entry added!") // remove the statefullness of this snippet unregisterThisSnippet() } case x => error(x) } } } val allAccounts = user.allAccounts.map(acct => (acct.id.toString, acct.name)) // Parse through the NodeSeq passed as "in" looking for tags // prefixed with "e". When found, replace the tag with a NodeSeq // according to the map below (name -> NodeSeq) bind("e", in, "account" -> select(allAccounts, Empty, id => account = id.toLong), "dateOf" -> text(Util.slashDate.format(new Date()).toString, date = _, "id" -> "entrydate"), "desc" -> text("Item Description", desc = _), "value" -> text("Value", value = _), "tags" -> text(tags, doTagsAndSubmit)) } // If no user logged in, return a blank Text node case _ => Text("") } }
<lift:surround with="default" at="content"> <lift:Accounts.detail eager_eval="true"> <div class="column span-24"> <h2>Summary</h2> <table><tr><th>Name</th><th>Balance</th></tr> <tr><td><acct:name /></td><td><acct:balance /></td></tr> </table> <div> <h3>Filters:</h3> <table><tr><th>Start Date</th><td><acct:startDate /></td> <th>End Date</th><td><acct:endDate /></td></tr> </table> </div> <div class="column span-24" > <h2>Transactions</h2> <lift:embed what="entry_table" /> </div> </lift:Accounts.detail> </lift:surround>
<table class="" border="0" cellpadding="0" cellspacing="1" width="100%"> <thead> <tr> <th>Date</th><th>Description</th><th>Tags</th><th>Value</th> <th>Balance</th> </tr> </thead> <tbody id="entry_table"> <acct:table> <acct:tableEntry> <tr><td><entry:date /></td><td><entry:desc /></td> <td><entry:tags /></td><td><entry:amt /></td> <td><entry:balance /></td> </tr> </acct:tableEntry> </acct:table> </tbody> </table>
package com.pocketchangeapp.snippet ... imports ... class Accounts { ... def buildExpenseTable(entries : List[Expense], template : NodeSeq) = { // Calls bind repeatedly, once for each Entry in entries entries.flatMap({ entry => bind("entry", chooseTemplate("acct", "tableEntry", template), "date" -> Text(Util.slashDate.format(entry.dateOf.is)), "desc" -> Text(entry.description.is), "tags" -> Text(entry.tags.map(_.tag.is).mkString(", ")), "amt" -> Text(entry.amount.toString), "balance" -> Text(entry.currentBalance.toString)) }) } ... }
package com.pocketchangeapp.snippet import ... standard imports ... import com.pocketchangeapp.model._ import com.pocketchangeapp.util.Util class Accounts { def detail (xhtml: NodeSeq) : NodeSeq = S.param("name") match { // If the "name" param was passed by the browser... case Full(acctName) => { // Look for an account by that name for the logged in user Account.findByName(User.currentUser.open_!, acctName) match { // If an account is returned (as a List) case acct :: Nil => { // Some closure state for the AJAX calls // Here is Lift’s "Box" in action: we are creating // variables to hold Date Boxes and initializing them // to "Empty" (Empty is a subclass of Box) var startDate : Box[Date] = Empty var endDate : Box[Date] = Empty // AJAX utility methods. Defined here to capture the closure // vars defined above def entryTable = buildExpenseTable( Expense.getByAcct(acct, startDate, endDate, Empty), xhtml) def updateStartDate (date : String) = { startDate = Util.parseDate(date, Util.slashDate.parse) JsCmds.SetHtml("entry_table", entryTable) } def updateEndDate (date : String) = { endDate = Util.parseDate(date, Util.slashDate.parse) JsCmds.SetHtml("entry_table", entryTable) } // Bind the data to the passed in XML elements with // prefix "acct" according to the map below. bind("acct", xhtml, "name" -> acct.name.asHtml, "balance" -> acct.balance.asHtml, "startDate" -> SHtml.ajaxText("", updateStartDate), "endDate" -> SHtml.ajaxText("", updateEndDate), "table" -> entryTable) } // An account name was provided but did not match any of // the logged in user’s accounts case _ => Text("Could not locate account " + acctName) } } // The S.param "name" was empty case _ => Text("No account name provided") } }
git clone git://github.com/tjweir/pocketchangeapp.git
(C) 2012 Lift 2.0 EditionWritten by Derek Chen-Becker, Marius Danciu and Tyler Weir