← Back to team overview

kolibri-discuss team mailing list archive

[Merge] lp:~asteinlein/kolibri/validation-improved into lp:kolibri

 

Anders Steinlein has proposed merging lp:~asteinlein/kolibri/validation-improved into lp:kolibri.

Requested reviews:
    Kolibri Dev (kolibri-dev)

See branch whiteboard and commit logs for details.
-- 
https://code.launchpad.net/~asteinlein/kolibri/validation-improved/+merge/5355
Your team Kolibri Discuss is subscribed to branch lp:kolibri.
=== modified file 'examples/wishlist/actions/Index.php'
--- examples/wishlist/actions/Index.php	2008-12-03 02:22:07 +0000
+++ examples/wishlist/actions/Index.php	2009-04-07 22:06:43 +0000
@@ -1,14 +1,16 @@
 <?php
 /**
- * Action for the display of the front page. Retrieves the wishlist items and returns the XSL page as the
- * result.
+ * Action for the display of the front page. Retrieves the wishlist items and returns the XSL
+ * page as the result. We also implement ModelAware so any validation errors when adding items
+ * are displayed.
  */
-class Index extends ActionSupport {
+class Index extends ActionSupport implements ModelAware {
+	public $model;
 	public $items;
 
 	/**
-	 * As the name implies, doGet() is called for GET request. It must return an instance of a Result class, in 
-	 * this case a XsltResult for a XSL transformation.
+	 * As the name implies, doGet() is called for GET request. It must return an instance of a
+	 * Result class, in this case a XsltResult for a XSL transformation.
 	 */
 	public function doGet () {
 		$dbSetup = new DatabaseSetup();
@@ -18,8 +20,10 @@
 		}
 
 		$items = Models::init('Item');
-		$this->items = $items->objects->findAll(); // Notice that this calls findAll() in the ItemDao class
-		return new XsltResult($this, '/index'); // Path relative to views directory, extension omitted
+		// Notice that this calls findAll() in the ItemDao class
+		$this->items = $items->objects->findAll(); 
+		// Path is relative to views directory, extension omitted
+		return new XsltResult($this, '/index'); 
 	}
 }
 ?>

=== modified file 'examples/wishlist/actions/items/Add.php'
--- examples/wishlist/actions/items/Add.php	2008-10-20 16:41:10 +0000
+++ examples/wishlist/actions/items/Add.php	2009-04-07 20:59:58 +0000
@@ -1,40 +1,35 @@
 <?php
 /**
- * Action for adding new item. We implement ModelAware to have the model object populated by request 
- * data, and ValidationAware to have it automatically validated.
+ * Action for adding new item. We implement ModelAware to have the model object populated by
+ * request data, and ValidationAware to have it automatically validated.
  */
 class Add extends ActionSupport implements ModelAware, ValidationAware {
 	/**
-	 * Defines the model class to instantiate, which will be populated by request data and put back into 
-	 * this variable.
+	 * Defines the model class to instantiate, which will be populated with request data and
+	 * put back into this property.
 	 */
 	public $model = 'Item';
 
 	/**
-	 * Any validation errors are contained herein. If emtpy, the model is valid according to its rules.
-	 */
-	public $errors;
-
-	/**
-	 * As the name implies, this handles POST.
+	 * As the name implies, this handles POST. It will only be called if the model validates,
+	 * else validationFailed() will be called instead.
 	 */
 	public function doPost () {
-		if (empty($this->errors)) {
-			/*
-			 * No validation errors are reported, so we can go ahead and save the model. Notice that $this->model 
-			 * now is a fully prepared model.
-			 */
-			if ($this->model->save()) {
-				$this->msg->setMessage('Item successfully added.');
-			}
-			return new RedirectResult($this, '/');
-		}
+		$this->model->save();
+		$this->msg->setMessage('Item successfully added.');
+		return new RedirectResult($this, '/');
+	}
 
-		/*
-		 * Validation errors found, so return the page again to display errors with the form populated. If we 
-		 * redirect, error messages and form data will be lost.
-		 */
-		return new XsltResult($this, '/index');
+	/**
+	 * This is called when validation fails, in order for us to redirect back to where the
+	 * form is presented. By using redirect instead of simply displaying the form now,
+	 * we conform to the Post-Redirect-Get webapp pattern, which among other things lets
+	 * users safely go Back/Forward and Refresh.
+	 */
+	public function validationFailed () {
+		// We could set a custom error message here if we want to override the default. I.e.:
+		// $this->msg->setMessage('The item could not be added to the wishlist', false);
+		return new RedirectResult($this, '/');
 	}
 }
 ?>

=== modified file 'examples/wishlist/conf/config.php'
--- examples/wishlist/conf/config.php	2009-03-27 23:31:04 +0000
+++ examples/wishlist/conf/config.php	2009-04-07 23:17:37 +0000
@@ -4,8 +4,8 @@
  * get by calling Config::get('key'), where key is the setting you want to return, i.e. 'mail'.
  */
 $config = array(
-		'webRoot'    => '',        // Change if not on root level. Prefix with slash if not empty, but no trailing!
-		'staticRoot' => '/static', // URI of static resources (can be another host as http://static.example.com)
+		'webRoot'    => 'http://localhost', // Must be absolute URI including scheme. No trailing slash!
+		'staticRoot' => '/static',          // URI of static resources (can be another host as http://static.example.com)
 		'locale'     => 'en_US.utf8',
 		'logging'    => array(
 			'enabled'  => false,   // When logging is disabled, errors are outputted directly. When enabled...

=== added file 'examples/wishlist/views/snippets/forms.xsl'
--- examples/wishlist/views/snippets/forms.xsl	1970-01-01 00:00:00 +0000
+++ examples/wishlist/views/snippets/forms.xsl	2009-04-07 22:06:43 +0000
@@ -0,0 +1,553 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet version="1.0"
+                xmlns="http://www.w3.org/1999/xhtml";
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
+                xmlns:exsl="http://exslt.org/common";
+                xmlns:func="http://exslt.org/functions";
+                xmlns:k="http://kolibriproject.com/xml";
+                extension-element-prefixes="exsl func">
+		
+	<!--
+		Executes the current form template, retrieving the Kolibri form definition which is subsequently
+		parsed into an XHTML form.
+		
+		@return NodeSet	The XHTML form described by the current form template.
+	-->
+	<func:function name="k:form">
+		<xsl:variable name="structure">
+			<xsl:call-template name="form" />
+		</xsl:variable>
+		
+		<func:result>
+			<xsl:apply-templates select="exsl:node-set($structure)/k:form" />
+		</func:result>
+	</func:function>
+	
+	<!--
+		Simple function to check if a context node is one of the supported Kolibri form field
+		elements.
+		
+		@return Boolean	true() if the context node is a Kolibri form field.
+	-->
+	<func:function name="k:is-form-field">
+		<func:result select="self::k:input or self::k:radio or self::k:checkbox or self::k:textarea
+			or self::k:select or self::k:hidden" />
+	</func:function>
+	
+	<!--
+		Generates attributes on the surrounding element of a form field.
+		A named template is used to create attributes conditionally.
+	-->
+	<xsl:template name="form-element-attributes">
+		<xsl:variable name="classes">
+			<!-- Add descriptive class names if the form field is required or contains errors -->
+			<xsl:if test="not(@required) or @required != 'false'">
+				<class>required</class>
+			</xsl:if>
+			<xsl:if test="k:has-error()">
+				<class>error</class>
+			</xsl:if>
+			<!-- Add class name for radio buttons, checkboxes and hidden fields (TODO: All form fields?) -->
+			<xsl:if test="self::k:radio or self::k:checkbox or self::k:hidden">
+				<class><xsl:value-of select="substring-after(name(), ':')" /></class>
+			</xsl:if>
+			<xsl:if test="position() mod 2 = 0">
+				<class>even</class>
+			</xsl:if>
+		</xsl:variable>
+		
+		<!-- Only create the attribute if $classes contains a string with nodes -->
+		<xsl:if test="string($classes)">
+			<xsl:attribute name="class">
+				<xsl:value-of select="k:string-list(exsl:node-set($classes)/*, ' ', ' ')" />
+			</xsl:attribute>
+		</xsl:if>
+	</xsl:template>
+	
+	<!--
+		Generates attributes on a form field, from the more general attributes like id and name to
+		the more specific like size or value.
+	-->
+	<xsl:template name="input-field-attributes">
+		<!-- Set ID attribute if specified -->
+		<xsl:if test="@id">
+			<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
+		</xsl:if>
+
+		<!--
+			Set name attribute to name attribute or id attribute. Store in variable to use as
+			identifier for the value attribute.
+		-->
+		<xsl:variable name="name">
+			<xsl:choose>
+				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
+				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
+			</xsl:choose>
+		</xsl:variable>
+		<xsl:attribute name="name"><xsl:value-of select="$name" /></xsl:attribute>
+		
+		<!-- Set type attribute automatically for radio buttons, check boxes and hidden fields. -->
+		<xsl:if test="self::k:radio or self::k:checkbox or self::k:hidden">
+			<xsl:attribute name="type"><xsl:value-of select="substring-after(name(), ':')" /></xsl:attribute>
+		</xsl:if>
+		<xsl:if test="self::k:input">
+			<xsl:attribute name="type">
+				<xsl:choose>
+					<xsl:when test="@type"><xsl:value-of select="@type" /></xsl:when>
+					<xsl:otherwise>text</xsl:otherwise>
+				</xsl:choose>
+			</xsl:attribute>
+			<xsl:attribute name="size">
+				<xsl:choose>
+					<xsl:when test="@size"><xsl:value-of select="@size" /></xsl:when>
+					<xsl:otherwise>30</xsl:otherwise>
+				</xsl:choose>
+			</xsl:attribute>
+			<xsl:if test="@maxlength">
+				<xsl:attribute name="maxlength"><xsl:value-of select="@maxlength" /></xsl:attribute>
+			</xsl:if>
+		</xsl:if>
+		<xsl:if test="self::k:select">
+			<xsl:attribute name="size">
+				<xsl:choose>
+					<xsl:when test="@size"><xsl:value-of select="@size" /></xsl:when>
+					<xsl:otherwise>1</xsl:otherwise>
+				</xsl:choose>
+			</xsl:attribute>
+			<xsl:if test="@multiple">
+				<xsl:attribute name="multiple">multiple</xsl:attribute>
+			</xsl:if>
+		</xsl:if>
+		
+		<!--
+			Set value attribute conditionally, it is required for radio buttons and check boxes.
+			For textareas we set the content of the generated element.
+		-->
+		<xsl:choose>
+			<xsl:when test="self::k:checkbox">
+				<xsl:variable name="value">
+					<xsl:choose>
+						<xsl:when test="@value"><xsl:value-of select="@value" /></xsl:when>
+						<xsl:otherwise>true</xsl:otherwise>
+					</xsl:choose>
+				</xsl:variable>
+				
+				<!--
+					Default value of a check box is simply 'true', which is also
+					supported as value of model property to automatically select the check box.
+				-->
+				<xsl:attribute name="value"><xsl:value-of select="$value" /></xsl:attribute>
+				<xsl:if test="k:model-value($name) = $value or @checked = 'true'">
+					<xsl:attribute name="checked">checked</xsl:attribute>
+				</xsl:if>
+			</xsl:when>
+			<xsl:when test="self::k:radio">
+				<xsl:attribute name="value"><xsl:value-of select="@value" /></xsl:attribute>
+				<xsl:if test="(@value and k:model-value($name) = @value) or @checked = 'true'">
+					<xsl:attribute name="checked">checked</xsl:attribute>
+				</xsl:if>
+			</xsl:when>
+			<xsl:when test="not(self::k:textarea) and not(self::k:select)">
+				<!--
+					For every normal form field the value is set through 'value' attribute,
+					model property value or simply the Kolibri form element's text content.
+				-->
+				<xsl:choose>
+					<xsl:when test="@value">
+						<xsl:attribute name="value"><xsl:value-of select="@value" /></xsl:attribute>
+					</xsl:when>
+					<xsl:when test="$model and string(k:model-value($name))">
+						<xsl:attribute name="value"><xsl:value-of select="k:model-value($name)" /></xsl:attribute>
+					</xsl:when>
+					<xsl:when test="string(text())">
+						<xsl:attribute name="value"><xsl:value-of select="text()" /></xsl:attribute>
+					</xsl:when>
+				</xsl:choose>
+			</xsl:when>
+			<xsl:when test="self::k:textarea">
+				<!-- Textareas has their text content as the value of the form field. -->
+				<xsl:choose>
+					<xsl:when test="$model and string(k:model-value($name))">
+						<xsl:value-of select="k:model-value($name)" />
+					</xsl:when>
+					<xsl:otherwise>
+						<xsl:value-of select="." />
+					</xsl:otherwise>
+				</xsl:choose>
+			</xsl:when>
+		</xsl:choose>
+		
+		<!-- Set disabled status if specified -->
+		<xsl:if test="not(self::k:hidden) and (@disabled = 'disabled' or @disabled = 'true')">
+			<xsl:attribute name="disabled">disabled</xsl:attribute>
+		</xsl:if>
+		
+		<!-- Set custom class attribute if specified -->
+		<xsl:variable name="cssClasses">
+			<xsl:if test="@class">
+				<class><xsl:value-of select="@class" /></class>
+			</xsl:if>
+			<xsl:choose>
+				<xsl:when test="@type">
+					<class><xsl:value-of select="@type" /></class>
+				</xsl:when>
+				<!-- Default type of an input field is text if @type doesn't exist -->
+				<xsl:when test="self::k:input">
+					<class>text</class>
+				</xsl:when>
+				<!-- Radio buttons and check boxes get 'radio' or 'checkbox' as class as well -->
+				<xsl:when test="self::k:radio or self::k:checkbox">
+					<class><xsl:value-of select="substring-after(name(), ':')" /></class>
+				</xsl:when>
+			</xsl:choose>
+		</xsl:variable>
+		<xsl:if test="string($cssClasses)">
+			<xsl:attribute name="class">
+				<xsl:value-of select="k:string-list(exsl:node-set($cssClasses)/*, ' ', ' ')" />
+			</xsl:attribute>
+		</xsl:if>
+	</xsl:template>
+
+	<!--
+		Generates the content of a form element; either a standalone form field, custom HTML or plain text.
+	-->
+	<xsl:template name="form-element-content">
+		<xsl:variable name="inCustomDiv" select="boolean(self::k:div)" />
+		<xsl:for-each select="*|text()">
+			<xsl:choose>
+				<xsl:when test="k:is-form-field() and $inCustomDiv">
+					<xsl:apply-templates select="." mode="standalone" />
+				</xsl:when>
+				<xsl:otherwise>
+					<xsl:apply-templates select="." />
+				</xsl:otherwise>
+			</xsl:choose>
+		</xsl:for-each>
+	</xsl:template>
+	
+	<!--
+		Prints out error messages for a form field. Radio buttons are a special case, where
+		errors will only be printed after the last radio button in a button group.
+	-->
+	<xsl:template name="field-errors">
+		<xsl:choose>
+			<xsl:when test="self::k:radio">
+				<!-- Only print out errors for the last radio button in a group -->
+				<xsl:if test=". = //k:radio[@name = current()/@name and last()]">
+					<xsl:apply-templates select="k:get-errors(@name)" />
+				</xsl:if>
+			</xsl:when>
+			<xsl:otherwise>
+				<xsl:variable name="id">
+					<xsl:choose>
+						<xsl:when test="@name or @id">
+							<xsl:choose>
+								<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
+								<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
+							</xsl:choose>
+						</xsl:when>
+						<xsl:when test="self::k:div">
+							<xsl:choose>
+								<xsl:when test="descendant::*/@name[1]">
+									<xsl:value-of select="descendant::*/@name[1]" />
+								</xsl:when>
+								<xsl:otherwise>
+									<xsl:value-of select="descendant::*/@id[1]" />
+								</xsl:otherwise>
+							</xsl:choose>
+						</xsl:when>
+					</xsl:choose>
+				</xsl:variable>
+				<xsl:apply-templates select="k:get-errors($id)" />
+			</xsl:otherwise>
+		</xsl:choose>
+	</xsl:template>
+	
+	<!--
+		Template for parsing <k:form> elements, describing the structure of an XHTML form, optionally
+		representing a Kolibri model.
+	-->
+	<xsl:template match="k:form">
+		<form action="{@action}" method="post">
+			<xsl:if test="@id">
+				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
+			</xsl:if>
+			<xsl:if test="@method">
+				<xsl:attribute name="method"><xsl:value-of select="@method" /></xsl:attribute>
+			</xsl:if>
+			<xsl:if test="@enctype">
+				<xsl:attribute name="enctype"><xsl:value-of select="@enctype" /></xsl:attribute>
+			</xsl:if>
+			
+			<!-- Generate form fields with k:form templates -->
+			<xsl:apply-templates select="*" />
+		</form>
+	</xsl:template>
+	
+	<!--
+		Simple fieldset template. Supports adding a legend through using a legend attribute on
+		k:fieldset (when no separate legend element exists). Also supports adding error class attribute
+		if a typical Kolibri ul element with errors exists within the fieldset.
+	-->
+	<xsl:template match="k:form/k:fieldset">
+		<fieldset>
+			<xsl:if test="@id">
+				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
+			</xsl:if>
+			<xsl:if test="*[name() = 'ul' and @class = 'errors']">
+				<xsl:attribute name="class">error</xsl:attribute>
+			</xsl:if>
+			<xsl:if test="@legend and not(*[name() = 'legend'])">
+				<legend><xsl:value-of select="@legend" /></legend>
+			</xsl:if>
+			
+			<!-- Generate form fields with k:form templates -->
+			<xsl:apply-templates select="*|text()" />
+		</fieldset>
+	</xsl:template>
+	
+	<!--
+		Template for k:div elements. Creates a div with a label element. The label will be linked to
+		the form field if it's the only field inside the k:div, otherwise a 'for' attribute will need
+		to be provided for the k:div element to indicate which field the label should be linked to.
+	-->
+	<xsl:template match="k:div">
+		<xsl:variable name="fields" select="descendant::*[self::k:input or self::k:textarea or
+			self::k:select or self::k:radio or self::k:checkbox]" />
+		
+		<div>
+			<xsl:call-template name="form-element-attributes" />
+			
+			<xsl:choose>
+				<xsl:when test="count($fields) > 1 and @label">
+					<label>
+						<xsl:if test="@for">
+							<xsl:attribute name="for"><xsl:value-of select="@for" /></xsl:attribute>
+						</xsl:if>
+						<xsl:value-of select="@label" />
+					</label>
+				</xsl:when>
+				<xsl:when test="count($fields) = 1">
+					<label>
+						<xsl:attribute name="for">
+							<xsl:choose>
+								<xsl:when test="@for"><xsl:value-of select="@for" /></xsl:when>
+								<xsl:otherwise><xsl:value-of select="$fields/@id" /></xsl:otherwise>
+							</xsl:choose>
+						</xsl:attribute>
+						<xsl:value-of select="$fields/@label" />
+					</label>
+				</xsl:when>
+			</xsl:choose>	
+
+			<!-- Create the actual form element(s) -->
+			<xsl:call-template name="form-element-content" />
+
+			<!-- List all validation errors if there are any -->
+			<xsl:call-template name="field-errors" />
+		</div>
+	</xsl:template>
+	
+	<!--
+		Template for all Kolibri form fields not wrapped in a k:div, except hidden fields.
+	-->
+	<xsl:template match="*[not(parent::k:div) and k:is-form-field() and not(self::k:hidden)]">
+		<!-- Fetch label preferably from attribute, otherwise from text content of form field -->
+		<xsl:variable name="labelContent">
+			<xsl:choose>
+				<xsl:when test="@label"><xsl:value-of select="@label" /></xsl:when>
+				<xsl:when test="string(text())"><xsl:value-of select="text()" /></xsl:when>
+			</xsl:choose>
+		</xsl:variable>
+		<div>
+			<xsl:call-template name="form-element-attributes" />
+			
+			<!-- Only create label if there's defined content for it -->
+			<xsl:if test="$labelContent">
+				<label>
+					<xsl:if test="@id">
+						<xsl:attribute name="for"><xsl:value-of select="@id" /></xsl:attribute>
+					</xsl:if>
+					<!-- Generate form field inside label for radio buttons and check boxes -->
+					<xsl:if test="self::k:radio or self::k:checkbox">
+						<xsl:apply-templates select="." mode="standalone" />
+					</xsl:if>
+					<xsl:value-of select="$labelContent" />
+				</label>
+			</xsl:if>
+			
+			<!--
+				Generate form field outside label for fields other than radio buttons and check boxes,
+				or for radio buttons and check boxes without a label
+			-->
+			<xsl:if test="not($labelContent) or (not(self::k:radio) and not(self::k:checkbox))">
+				<xsl:apply-templates select="." mode="standalone" />
+			</xsl:if>
+			
+			<!-- List all validation errors if there are any -->
+			<xsl:call-template name="field-errors" />
+		</div>
+	</xsl:template>
+	
+	<!--
+		Simple template for creating a hidden form element outside k:div.
+	-->
+	<xsl:template match="k:hidden">
+		<xsl:apply-templates select="." mode="standalone" />
+	</xsl:template>
+	
+	<!--
+		Creates a select box from a k:select element with either Kolibri's k:option element
+		or a simple custom XML structure as data for the option elements.
+		If a custom XML structure is used the 'value' and 'text' attributes need to be supplied
+		for the k:select element, containing the name of the XML node or attribute which contains
+		each option's value and text.
+	-->
+	<xsl:template match="k:select" mode="standalone">
+		<!-- Find model property identifier and value of the selected option -->
+		<xsl:variable name="name">
+			<xsl:choose>
+				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
+				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
+			</xsl:choose>
+		</xsl:variable>
+		<xsl:variable name="selected" select="k:model-value($name)" />
+
+		<select>
+			<xsl:call-template name="input-field-attributes" />
+			
+			<xsl:choose>
+				<xsl:when test="k:optgroup or k:option">
+					<!-- Explicit k:option elements defined -->
+					<xsl:apply-templates select="*[self::k:optgroup or self::k:option]">
+						<xsl:with-param name="selected" select="$selected" />
+					</xsl:apply-templates>
+				</xsl:when>
+				<xsl:otherwise>
+					<!--
+						Use the node set inside k:select as data providers for option elements,
+						using the 'value' and 'text' attribute on k:select to get child node values
+						for option elements.
+					-->
+					<xsl:variable name="valueNode" select="@value" />
+					<xsl:variable name="textNode" select="@text" />
+					
+					<xsl:for-each select="*">
+						<xsl:variable name="value">
+							<xsl:choose>
+								<xsl:when test="$valueNode">
+									<xsl:value-of select="descendant::*[name() = $valueNode]" />
+								</xsl:when>
+								<xsl:otherwise><xsl:value-of select="text()" /></xsl:otherwise>
+							</xsl:choose>
+						</xsl:variable>
+						<xsl:variable name="text">
+							<xsl:choose>
+								<xsl:when test="$textNode">
+									<xsl:value-of select="descendant::*[name() = $textNode]" />
+								</xsl:when>
+								<xsl:otherwise><xsl:value-of select="text()" /></xsl:otherwise>
+							</xsl:choose>
+						</xsl:variable>
+						
+						<option value="{$value}">
+							<xsl:if test="$value = $selected">
+								<xsl:attribute name="selected">selected</xsl:attribute>
+							</xsl:if>
+							<xsl:value-of select="$text" />
+						</option>
+					</xsl:for-each>
+				</xsl:otherwise>
+			</xsl:choose>
+		</select>
+	</xsl:template>
+	
+	<xsl:template match="k:select/k:optgroup">
+		<xsl:param name="selected" />
+		<optgroup label="{@label}">
+			<xsl:apply-templates select="k:option">
+				<xsl:with-param name="selected" select="$selected" />
+			</xsl:apply-templates>
+		</optgroup>
+	</xsl:template>
+	
+	<xsl:template match="k:option">
+		<xsl:param name="selected" />
+		<option value="{@value}">
+			<xsl:if test="@selected = 'true' or @value = $selected">
+				<xsl:attribute name="selected">selected</xsl:attribute>
+			</xsl:if>
+			<xsl:choose>
+				<xsl:when test="string(.)">
+					<xsl:value-of select="." />
+				</xsl:when>
+				<xsl:otherwise><xsl:value-of select="@value" /></xsl:otherwise>
+			</xsl:choose>
+		</option>
+	</xsl:template>
+	
+	<!--
+		Creates a textarea from k:textarea.
+	-->
+	<xsl:template match="k:textarea" mode="standalone">
+		<textarea cols="30" rows="10">
+			<!-- Allow overriding default values for cols and rows -->
+			<xsl:if test="@cols">
+				<xsl:attribute name="cols"><xsl:value-of select="@cols" /></xsl:attribute>
+			</xsl:if>
+			<xsl:if test="@rows">
+				<xsl:attribute name="rows"><xsl:value-of select="@rows" /></xsl:attribute>
+			</xsl:if>
+			<xsl:call-template name="input-field-attributes" />
+		</textarea>
+	</xsl:template>
+
+	<!--
+		Creates input elements for text/password fields, check boxes, radio buttons and hidden fields from
+		k:input, k:checkbox, k:radio and k:hidden fields.
+	-->
+	<xsl:template match="k:input|k:checkbox|k:radio|k:hidden" mode="standalone">
+		<input>
+			<xsl:call-template name="input-field-attributes" />
+		</input>
+	</xsl:template>
+
+	<!--
+		Convenience template for creating the submit section of a form. Supplies the
+		ID of a model object through a hidden field if a model object exists.
+	-->
+	<xsl:template match="k:submit">
+		<div class="submit">
+			<!-- If a current model element exists, supply the model ID through a hidden 'original' field -->
+			<xsl:if test="$model and $model/original">
+				<input type="hidden" name="original" value="{$model/original}" />
+			</xsl:if>
+			<!-- Generate any other hidden fields -->
+			<xsl:apply-templates select="k:hidden" />
+
+			<span class="submit">
+				<button type="submit" name="{@name}">
+					<xsl:attribute name="name">
+						<xsl:choose>
+							<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
+							<xsl:when test="@id"><xsl:value-of select="@id" /></xsl:when>
+							<xsl:otherwise>save</xsl:otherwise>
+						</xsl:choose>
+					</xsl:attribute>
+					<xsl:value-of select="@value|@label" />
+				</button>
+			</span>
+		</div>
+	</xsl:template>
+	
+	<!--
+		Special templates to allow normal (X)HTML elements to be mixed in with
+		k:form and it's related elements.
+	-->
+	<xsl:template match="*[ancestor::k:form]" priority="-0.5">
+		<xsl:element name="{name()}">
+			<xsl:copy-of select="@*" />
+			<xsl:call-template name="form-element-content" />
+		</xsl:element>
+	</xsl:template>
+</xsl:stylesheet>

=== modified file 'examples/wishlist/views/snippets/kolibri.xsl'
--- examples/wishlist/views/snippets/kolibri.xsl	2008-10-20 16:41:10 +0000
+++ examples/wishlist/views/snippets/kolibri.xsl	2009-04-07 22:13:36 +0000
@@ -12,9 +12,11 @@
                 xmlns:k="http://kolibriproject.com/xml";
                 extension-element-prefixes="exsl func">
 
-	<!-- Makes the XML structures for model and errors available for the custom element templates -->
+	<xsl:include href="message.xsl" />
+	<xsl:include href="forms.xsl" />
+	
+	<!-- Makes the XML structures for model available for the custom element templates -->
 	<xsl:variable name="model" select="/result/model" />
-	<xsl:variable name="errors" select="/result/errors" />
 		
 	<func:function name="k:number">
 		<xsl:param name="value" />
@@ -61,6 +63,21 @@
 		</func:result>
 	</func:function>
 	
+	<func:function name="k:truncate">
+		<xsl:param name="str" />
+		<xsl:param name="maxlen" />
+		
+		<xsl:choose>
+			<xsl:when test="($maxlen + 1) > string-length($str)">
+				<func:result select="$str" />
+			</xsl:when>
+			<xsl:otherwise>
+				<func:result>
+					<xsl:value-of select="substring($str, 1, $maxlen - 1)" />…
+				</func:result>
+			</xsl:otherwise>
+		</xsl:choose>
+	</func:function>
 	<!--
 		Convenience function for linking to an external CSS file from the configured root of static files.
 		
@@ -78,7 +95,11 @@
 	
 	<!--
 		Fetches the value of a model attribute.
-	
+		
+		TODO: Generalize to k:object-value, where object by default is $model. This way,
+		:: attributes could be returned recursively (since objects/arrays are the same in XML).
+		And the function could be used for more than models.
+		
 		@param String attribute	The name of the attribute in the model.
 		@return NodeSet	The nodeset for the value of the model attribute.
 	-->
@@ -86,6 +107,12 @@
 		<xsl:param name="attribute" select="@id" />
 		
 		<xsl:choose>
+			<xsl:when test="contains($attribute, '[') and not(contains($attribute, '[]'))">
+				<xsl:variable name="prop" select="substring-before($attribute, '[')" />
+				<xsl:variable name="child" select="substring-before(substring-after($attribute, '['), ']')" />
+				
+				<func:result select="$model/*[name() = $prop]/*[name() = $child]" />
+			</xsl:when>
 			<xsl:when test="contains($attribute, '::')">
 				<xsl:variable name="prop" select="substring-before($attribute, '::')" />
 				<xsl:variable name="child">
@@ -118,7 +145,17 @@
 				<func:result select="$model/*[name() = $prop]/*[position() = $pos]/*[name() = $child]" />
 			</xsl:when>
 			<xsl:otherwise>
-				<func:result select="$model/*[name() = $attribute]" />
+				<xsl:variable name="pureName">
+					<xsl:choose>
+						<xsl:when test="contains($attribute, '[')">
+							<xsl:value-of select="substring-before($attribute, '[')" />
+						</xsl:when>
+						<xsl:otherwise>
+							<xsl:value-of select="$attribute" />
+						</xsl:otherwise>
+					</xsl:choose>
+				</xsl:variable>
+				<func:result select="$model/*[name() = $pureName]" />
 			</xsl:otherwise>
 		</xsl:choose>
 	</func:function>
@@ -136,7 +173,7 @@
 			<xsl:value-of disable-output-escaping="yes" select="$errors/*[name() = $attribute]" />
 		</xsl:variable-->
 
-		<func:result select="$errors/*[name() = $attribute]" />
+		<func:result select="$model/errors/*[name() = $attribute]" />
 	</func:function>
 	
 	<!--
@@ -156,409 +193,17 @@
 			</xsl:choose>
 		</xsl:param>
 			
-		<func:result select="boolean($errors/*[name() = $attribute])" />
-	</func:function>
-	
-	<!--
-		Executes the current form template, retrieving the Kolibri form definition which is subsequently
-		parsed into an XHTML form.
-		
-		@return NodeSet	The XHTML form described by the current form template.
-	-->
-	<func:function name="k:form">
-		<xsl:variable name="structure">
-			<xsl:call-template name="form" />
-		</xsl:variable>
-		
-		<func:result>
-			<xsl:apply-templates select="exsl:node-set($structure)" />
-		</func:result>
-	</func:function>
-	
-	<!-- General attributes on the surrounding element for a complete form field -->
-	<xsl:attribute-set name="form-field">
-		
-		<!-- Add descriptive class names if the form field is required or contains errors -->
-		<xsl:attribute name="class">
-			<xsl:choose>
-				<xsl:when test="@required and k:has-error()">
-					<xsl:text>required error</xsl:text>
-				</xsl:when>
-				<xsl:when test="@required">
-					<xsl:text>required</xsl:text>
-				</xsl:when>
-				<xsl:when test="k:has-error()">
-					<xsl:text>error</xsl:text>
-				</xsl:when>
-			</xsl:choose>
-			<xsl:if test="self::k:radio or self::k:checkbox or self::k:hidden">
-				<xsl:text> </xsl:text><xsl:value-of select="substring-after(name(), ':')" />
-			</xsl:if>
-			<xsl:if test="position() mod 2 = 0">
-				<xsl:text> even</xsl:text>
-			</xsl:if>
-		</xsl:attribute>
-	</xsl:attribute-set>
-
-	<!--
-		Template for parsing <k:form> elements, describing the structure of an XHTML form, optionally
-		representing a Kolibri model.
-	-->
-	<xsl:template match="k:form">
-		<!-- Normal form attributes default to the attributes on the k:form element -->
-		<xsl:param name="action" select="@action" />
-		<xsl:param name="method" select="@method" />
-		<xsl:param name="enctype" select="@enctype" />
-		
-		<form action="{$action}" method="post">
-			<xsl:if test="@id">
-				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="$method">
-				<xsl:attribute name="method"><xsl:value-of select="$methd" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="$enctype">
-				<xsl:attribute name="enctype"><xsl:value-of select="$enctype" /></xsl:attribute>
-			</xsl:if>
-			
-			<!-- Generate form fields for elements we support -->
-			<xsl:apply-templates select="k:fieldset|k:div|k:input|k:select|k:radio|k:checkbox|k:textarea|k:hidden|k:submit" />
-		</form>
-	</xsl:template>
-	
-	<xsl:template match="k:fieldset">
-		<xsl:variable name="customErrors" select="*[local-name() = 'ul'][@class = 'errors']" />
-		
-		<fieldset>
-			<xsl:if test="@id">
-				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="$customErrors">
-				<xsl:attribute name="class">error</xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@legend">
-				<legend><xsl:value-of select="@legend" /></legend>
-			</xsl:if>
-			<xsl:if test="k:legend">
-				<legend>
-					<label>
-						<xsl:apply-templates select="k:legend/*[1]/self::node()" mode="standalone" />
-						<xsl:copy-of select="k:legend/text()" />
-					</label>
-				</legend>
-			</xsl:if>
-			
-			<xsl:apply-templates select="k:div|k:input|k:select|k:radio|k:checkbox|k:textarea|k:hidden" />
-			
-			<xsl:copy-of select="$customErrors" />
-		</fieldset>
-	</xsl:template>
-	
-	<!--
-		Matches k:div elements, and k:input or k:textarea which are not contained in a k:div.
-	-->
-	<xsl:template match="k:div |
-			*[name() != 'k:div']/*[self::k:input or self::k:select or self::k:textarea or
-				self::k:radio or self::k:checkbox or self::k:hidden]">
-		<xsl:variable name="fields" select="k:input|k:select|k:radio|k:checkbox|k:textarea|k:hidden" />
-		<xsl:variable name="content">
-			<xsl:choose>
-				<xsl:when test="self::k:div">
-					<!-- Create the first element with ID equal to the label "for" attribute -->
-					<xsl:apply-templates select="$fields[position() = 1]" mode="standalone">
-						<xsl:with-param name="id" select="@id" />
-					</xsl:apply-templates>
-					<!-- Create the rest of the field elements, if any -->
-					<xsl:apply-templates select="$fields[position() > 1]" mode="standalone" />
-				</xsl:when>
-				<xsl:otherwise>
-					<xsl:apply-templates select="." mode="standalone" />
-				</xsl:otherwise>
-			</xsl:choose>			
-		</xsl:variable>
-
-		<div xsl:use-attribute-sets="form-field">
-			<xsl:choose>
-				<xsl:when test="self::k:div or self::k:input or self::k:select or self::k:textarea">
-					<label for="{@id}"><xsl:value-of select="@label" /></label>
-					<xsl:copy-of select="$content" />
-				</xsl:when>
-				<xsl:when test="self::k:hidden">
-					<xsl:copy-of select="$content" />
-				</xsl:when>
-				<xsl:otherwise>
-					<xsl:variable name="label">
-						<xsl:choose>
-							<xsl:when test="@label"><xsl:value-of select="@label" /></xsl:when>
-							<xsl:otherwise><xsl:value-of select="text()" /></xsl:otherwise>
-						</xsl:choose>
-					</xsl:variable>
-
-					<label for="{@id}">
-						<xsl:copy-of select="$content" />
-						<xsl:value-of select="$label" />
-					</label>
-				</xsl:otherwise>
-			</xsl:choose>
-
-			<!-- Show inline info for the form field -->
-			<xsl:apply-templates select="k:info" />
-
-			<!-- List all validation errors if there are any -->
-			<xsl:choose>
-				<xsl:when test="self::k:radio">
-					<!--
-						Only print out errors if this radio button is the last one of those
-						grouped together with it.
-					-->
-					<xsl:variable name="grouping" select="@name" />
-					<xsl:if test=". = //k:radio[@name = $grouping][last()]">
-						<xsl:apply-templates select="k:get-errors($grouping)" />
-					</xsl:if>
-				</xsl:when>
-				<xsl:when test="self::k:div">
-					<xsl:variable name="id">
-						<xsl:choose>
-							<xsl:when test="$fields[1]/@id"><xsl:value-of select="$fields[1]/@id" /></xsl:when>
-							<xsl:otherwise><xsl:value-of select="$fields[1]/@name" /></xsl:otherwise>
-						</xsl:choose>
-					</xsl:variable>
-					<xsl:apply-templates select="k:get-errors($id)" />
-				</xsl:when>
-				<xsl:otherwise>
-					<xsl:apply-templates select="k:get-errors(@id)" />
-				</xsl:otherwise>
-			</xsl:choose>
-		</div>
-	</xsl:template>
-
-	<xsl:template match="k:info">
-		<p class="info">
-			<xsl:copy-of select="./child::node()" />
-		</p>
-	</xsl:template>
-	
-	<xsl:template match="k:input" mode="standalone">
-		<xsl:param name="id" select="@id" />
-		
-		<xsl:variable name="name">
-			<xsl:choose>
-				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		<xsl:variable name="type">
-			<xsl:choose>
-				<xsl:when test="@type"><xsl:value-of select="@type" /></xsl:when>
-				<xsl:otherwise>text</xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		<xsl:variable name="value">
-			<xsl:choose>
-				<xsl:when test="@value"><xsl:value-of select="@value" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="text()" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		<xsl:variable name="class">
-			<xsl:if test="@class"><xsl:value-of select="@class" /></xsl:if>
-			<xsl:value-of select="concat(' ', $type)" />
-		</xsl:variable>
-
-		<input id="{$id}" name="{$name}" type="{$type}" size="30" value="{$value}" class="{$class}">
-			<!-- Allow overriding default values for name, type and size -->
-			<xsl:if test="@size">
-				<xsl:attribute name="size"><xsl:value-of select="@size" /></xsl:attribute>
-			</xsl:if>
-			
-			<!-- Set optional attributes -->
-			<xsl:if test="@disabled = 'disabled'">
-				<xsl:attribute name="disabled">disabled</xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@maxlength">
-				<xsl:attribute name="maxlength"><xsl:value-of select="@maxlength" /></xsl:attribute>
-			</xsl:if>
-			
-			<!-- If a current model exists we override the default field value -->
-			<xsl:if test="$model and k:model-value($name)">
-				<xsl:attribute name="value"><xsl:value-of select="k:model-value($name)" /></xsl:attribute>
-			</xsl:if>
-		</input>
-	</xsl:template>
-	
-	<xsl:template match="k:select" mode="standalone">
-		<xsl:variable name="name">
-			<xsl:choose>
-				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		<xsl:variable name="size">
-			<xsl:choose>
-				<xsl:when test="@size"><xsl:value-of select="@size" /></xsl:when>
-				<xsl:otherwise>1</xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		
-		<select name="{$name}" size="{$size}">
-			<xsl:if test="@id">
-				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
-			</xsl:if>
-			<xsl:variable name="selected">
-				<xsl:value-of select="$model/*[local-name() = $name]" />
-			</xsl:variable>
-			
-			<xsl:choose>
-				<xsl:when test="k:option">
-					<!-- Explicit k:option elements defined -->
-					<xsl:for-each select="k:option">
-						<option value="{@value}">
-							<xsl:if test="@value = $selected">
-								<xsl:attribute name="selected">selected</xsl:attribute>
-							</xsl:if>
-							<xsl:value-of select="." />
-						</option>
-					</xsl:for-each>
-				</xsl:when>
-				<xsl:otherwise>
-					<!--
-						Use the node set inside k:select as data providers for option elements,
-						using the 'value' and 'text' attribute on k:select to get child node values
-						for option elements.
-					-->
-					<xsl:variable name="valueNode" select="@value" />
-					<xsl:variable name="textNode" select="@text" />
-					
-					<xsl:for-each select="*">
-						<xsl:variable name="value" select="*[local-name() = $valueNode]" />
-						<option value="{$value}">
-							<xsl:if test="$value = $selected">
-								<xsl:attribute name="selected">selected</xsl:attribute>
-							</xsl:if>
-							<xsl:value-of select="*[local-name() = $text]" />
-						</option>
-					</xsl:for-each>
-				</xsl:otherwise>
-			</xsl:choose>
-		</select>
-	</xsl:template>
-	
-	<xsl:template match="k:textarea" mode="standalone">
-		<textarea id="{@id}" name="{@id}" cols="30" rows="10">
-			<!-- Allow overriding default values for cols and rows -->
-			<xsl:if test="@cols">
-				<xsl:attribute name="cols"><xsl:value-of select="@cols" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@rows">
-				<xsl:attribute name="rows"><xsl:value-of select="@rows" /></xsl:attribute>
-			</xsl:if>
-			
-			<!-- Set optional attributes -->
-			<xsl:if test="@disabled = 'disabled'">
-				<xsl:attribute name="disabled">disabled</xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@class">
-				<xsl:attribute name="class"><xsl:value-of select="@class" /></xsl:attribute>
-			</xsl:if>
-			
-			<!-- If a current model exists we override the default field value -->
-			<xsl:choose>
-				<xsl:when test="$model">
-					<xsl:value-of select="k:model-value(@id)" />
-				</xsl:when>
-				<xsl:otherwise>
-					<xsl:value-of select="@value | ." />
-				</xsl:otherwise>
-			</xsl:choose>
-		</textarea>
-	</xsl:template>
-
-	<!--
-		Creates a simple checkbox from a k:checkbox element.
-	-->
-	<xsl:template match="k:checkbox" mode="standalone">
-		<xsl:variable name="name">
-			<xsl:choose>
-				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		
-		<input type="checkbox" name="{$name}" value="true">
-			<xsl:if test="@id">
-				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@value">
-				<xsl:attribute name="value"><xsl:value-of select="@value" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="k:model-value(@id) = 'true' or @checked = 'true'">
-				<xsl:attribute name="checked">checked</xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@disabled = 'disabled' or @disabled = 'true'">
-				<xsl:attribute name="disabled">disabled</xsl:attribute>
-			</xsl:if>
-		</input>
-	</xsl:template>
-
-	<xsl:template match="k:radio" mode="standalone">
-		<xsl:variable name="name">
-			<xsl:choose>
-				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		
-		<input type="radio" name="{$name}" value="{@value}">
-			<xsl:if test="@id">
-				<xsl:attribute name="id"><xsl:value-of select="@id" /></xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@checked and @checked = 'true'">
-				<xsl:attribute name="checked">checked</xsl:attribute>
-			</xsl:if>
-			<xsl:if test="@disabled = 'disabled' or @disabled = 'true'">
-				<xsl:attribute name="disabled">disabled</xsl:attribute>
-			</xsl:if>
-		</input>
-	</xsl:template>
-	
-	<!-- Creates a hidden form field described by a k:hidden field. Exists mostly for completeness right now -->
-	<xsl:template match="k:hidden" mode="standalone">
-		<xsl:variable name="name">
-			<xsl:choose>
-				<xsl:when test="@name"><xsl:value-of select="@name" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="@id" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		<xsl:variable name="value">
-			<xsl:choose>
-				<xsl:when test="@value"><xsl:value-of select="@value" /></xsl:when>
-				<xsl:otherwise><xsl:value-of select="k:model-value($name)" /></xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		
-		<input type="hidden" name="{$name}" value="{$value}" />
-	</xsl:template>
-	
-	<xsl:template match="k:submit">
-		<xsl:variable name="cssClass">
-			<xsl:choose>
-				<xsl:when test="@class"><xsl:value-of select="@class" /></xsl:when>
-				<!-- TODO: Change to something more generic than BEV classes -->
-				<xsl:otherwise>knapp kjop</xsl:otherwise>
-			</xsl:choose>
-		</xsl:variable>
-		
-		<div class="submit">
-			<!-- If a current model element exists, supply the model ID through a hidden 'original' field -->
-			<xsl:if test="$model">
-				<input type="hidden" name="original" value="{$model/original}" />
-			</xsl:if>
-			<!-- Generate any other hidden fields -->
-			<xsl:apply-templates select="k:hidden" mode="standalone" />
-
-			<!-- TODO: Remove custom HTML for bev and replace with a general override mechanism -->
-			<span class="{$cssClass}">
-				<button type="submit" name="{@name}"><xsl:value-of select="@value" /></button>
-			</span>
-		</div>
+		<func:result select="boolean($model/errors/*[name() = $attribute])" />
+	</func:function>
+	
+	<!--
+		Prints out a list of errors for an element.
+	-->
+	<xsl:template match="errors/*">
+		<ul class="errors">
+			<xsl:for-each select="*">
+				<li><xsl:value-of select="." /></li>
+			</xsl:for-each>
+		</ul>
 	</xsl:template>
 </xsl:stylesheet>

=== modified file 'src/actions/ModelAware.php'
--- src/actions/ModelAware.php	2008-10-20 16:41:10 +0000
+++ src/actions/ModelAware.php	2009-04-07 22:06:43 +0000
@@ -1,22 +1,22 @@
 <?php
 /**
- * This interface is used by actions that want a model auto-instantiated and populated with values from
- * request parameters. The action must expose a public <code>$model</code> property which will hold the
- * populated model. The <code>ModelInterceptor</code> must be configured for the action for this to have
- * any effect.
- *
- * @version		$Id: ModelAware.php 1523 2008-07-09 23:32:14Z anders $
+ * This interface is used by actions that want a model auto-instantiated and populated with
+ * values from request parameters (or after a redirect from validation, the session). The
+ * action must expose a public <code>$model</code> property which will hold the populated model.
+ * 
+ * For a model to be instantiated and populated, the <code>ModelInterceptor</code> must be
+ * configured for the action, and the action must provide the name (or object) of the model
+ * to use. This can be done either by setting a default value on the $model property like so:
+ *
+ *     public $model = 'ModelName';
+ *
+ * Or, if a model with inner models should be created:
+ *
+ *     public $model = array('MainModelName', 'propertyInModel' => array('AnotherModelName'));
+ *
+ * Alternatively, you can return an already instantiated model by implementing a
+ * <code>getModel()</code> method. If present, this takes precedence over names specified in
+ * $model.
  */
-interface ModelAware {
-	/**
-	 * Returns the name of the model to instantiate and populate, or an array with the names and structure
-	 * of models if the main model contains other models. If an array is to be returned, it must have a
-	 * structure similar to the following example (it can be as deep as you want).
-	 *
-	 *     array('MainModelName', 'propertyInModel' => array('AnotherModelName'))
-	 *
-	 * @return mixed	Model class to instantiate.
-	 */
-	//public function getModelName ();
-}
+interface ModelAware {}
 ?>

=== modified file 'src/conf/autoload.php'
--- src/conf/autoload.php	2008-12-08 20:36:09 +0000
+++ src/conf/autoload.php	2009-04-08 15:34:28 +0000
@@ -28,7 +28,7 @@
 		'SessionInterceptor'     => '/interceptors/SessionInterceptor.php',
 		'UploadInterceptor'      => '/interceptors/UploadInterceptor.php',
 		'UtilsInterceptor'       => '/interceptors/UtilsInterceptor.php',
-		'ValidatorInterceptor'   => '/interceptors/ValidatorInterceptor.php',
+		'ValidationInterceptor'  => '/interceptors/ValidationInterceptor.php',
 		'TransactionInterceptor' => '/interceptors/TransactionInterceptor.php',
 		'DataProvided'           => '/models/DataProvided.php',
 		'ModelProxy'             => '/models/ModelProxy.php',

=== modified file 'src/conf/config.php'
--- src/conf/config.php	2009-02-22 02:42:02 +0000
+++ src/conf/config.php	2009-04-07 23:17:37 +0000
@@ -5,8 +5,8 @@
  * Config::get('key'), where key is the setting you want to return, i.e. 'mail'.
  */
 $config = array(
-		'webRoot'    => '',        // Change if not on root level. No trailing slash!
-		'staticRoot' => '/static', // URI of static resources (can be another host as http://static.example.com)
+		'webRoot'    => 'http://localhost', // Must be absolute URI including scheme. No trailing slash!
+		'staticRoot' => '/static',          // URI of static resources (can be another host as http://static.example.com)
 		'locale'     => 'en_US.utf8',
 		'logging'    => array(
 			'enabled'  => false

=== modified file 'src/conf/interceptors.php'
--- src/conf/interceptors.php	2008-12-11 15:10:26 +0000
+++ src/conf/interceptors.php	2009-04-08 15:34:28 +0000
@@ -6,7 +6,7 @@
  */
 $interceptors = array(
 		'message'     => 'MessageInterceptor',
-		'validation'  => 'ValidatorInterceptor',
+		'validation'  => 'ValidationInterceptor',
 		'error'       => array(
 				'ErrorInterceptor' => array('result' => 'PhpResult', 'view' => '/error')
 		),

=== modified file 'src/interceptors/MessageInterceptor.php'
--- src/interceptors/MessageInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/MessageInterceptor.php	2009-04-07 22:47:54 +0000
@@ -2,11 +2,8 @@
 require(ROOT . '/lib/Message.php');
 
 /**
- * Interceptor which provides the target action, if <code>MessageAware</code>, with a facility to give
- * the user status messages. This interceptor should be defined early in the interceptor stack,
- * ideally after the <code>SessionInterceptor</code> and before the <code>ErrorInterceptor</code>.
- * 
- * @version		$Id: MessageInterceptor.php 1518 2008-06-30 23:43:38Z anders $
+ * Interceptor which provides the target action, if <code>MessageAware</code>, with a facility
+ * to give the user status messages.
  */
 class MessageInterceptor extends AbstractInterceptor {
 	/**
@@ -16,25 +13,31 @@
 		$action = $dispatcher->getAction();
 
 		if ($action instanceof MessageAware) {
-			$action->msg = Message::getInstance();
+			// If a previous message is set in the session, put it into the action
+			if ($action instanceof SessionAware && isset($action->session['message'])) {
+				$action->msg = $action->session['message'];
+				$action->session->remove('message');
+			}
+			// Otherwise create a new instance
+			else {
+				$action->msg = Message::getInstance();
+			}
 		}
 
 		$result = $dispatcher->invoke();
-		$this->checkMessageInSession($action);
+
+		/*
+		 * If we are about to redirect and a message has been set, save it temporarily in the
+		 * session so it can be retrieved in the new location.
+		 */
+		if ($result instanceof RedirectResult
+				&& $action instanceof SessionAware
+				&& $action instanceof MessageAware
+				&& !$action->msg->isEmpty()) {
+			$action->session['message'] = $action->msg;
+		}
+
 		return $result;
 	}
-
-	/**
-	 * Checks to see if the session has a message stored while the action do not. If so, the
-	 * message is injected into the action message and removed from the session.
-	 */
-	private function checkMessageInSession ($action) {
-		if ($action instanceof SessionAware && $action instanceof MessageAware && $action->msg->isEmpty()) {
-			if (isset($action->session['message'])) {
-				$action->msg = $action->session['message'];
-				unset($action->session['message']);
-			}
-		}
-	}
 }
 ?>

=== modified file 'src/interceptors/ModelInterceptor.php'
--- src/interceptors/ModelInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/ModelInterceptor.php	2009-04-07 22:06:43 +0000
@@ -1,12 +1,18 @@
 <?php
 /**
- * Interceptor which prepares a model with data from the request parameters.
- *
- * The target action must be <code>ModelAware</code> and return the names of the model (with any
- * inner models) we should prepare. After instantiating the specified model we loop through request
- * parameters and populate the model before it is set back into the action.
- * 
- * @version		$Id: ModelInterceptor.php 1554 2008-09-25 15:35:37Z anders $
+ * Interceptor which prepares a model with data from the request parameters, or extracts a model
+ * which was temporarily stored in the session. The target action must be
+ * <code>ModelAware</code> to subscribe to any of this interceptor's functionality.
+ *
+ * This interceptor works in two "modes": It either extracts a model already present in the
+ * session if the request is a GET request, or it instantiates and populates a model with data
+ * from the request parameters. For the latter to work, the target action must provide the name
+ * of the model (along with any inner models) in a public <code>$model</code> property, or
+ * return a pre-instantiated model from a <code>getModel()</code> method. We then loop through
+ * request parameters and populate the model.
+ *
+ * Regardless of the "mode", the model found/prepared is put into the <code>$model</code>
+ * property of the action.
  */
 class ModelInterceptor extends AbstractInterceptor {
 	private $modelNames = array();
@@ -18,28 +24,54 @@
 		$action = $dispatcher->getAction();
 
 		if ($action instanceof ModelAware) {
-			if (method_exists($action, 'getModel')) {
-				// The action supplies an already instantiated model
-				$model = $action->getModel();
-			}
+			// We depend on a $model-property in ModelAware actions
+			if (!property_exists($action, 'model')) {
+				$class = get_class($action);
+				throw new Exception('Action ' . $class
+						. ' is ModelAware and must define a public $model property.');
+			}
+
+			/*
+			 * If model is availible from session we use that, but only if this is a GET
+			 * request. The reason for this is that any new POST-submit of a form should
+			 * take precedence, to stop any invalid model in the session to override the newly
+			 * POSTed.
+			 */
+			if ($dispatcher->getRequest()->getMethod() == 'GET'
+					&& $action instanceof SessionAware && isset($action->session['model'])) {
+				$action->model = $action->session['model'];
+				// Model has been extracted, remove it from session
+				$action->session->remove('model');
+			}
+			// Otherwise prepare a model from request parameters
 			else {
-				// The action supplies model class name(s), so we must instantiate
-				$model = $this->instantiateModel($action->model);
-			}
-
-			foreach ($dispatcher->getRequest() as $param => $value) {
-				if (strpos($param, '::') !== false) {
-					// Parameter is a property path to inner models. Explode the path and populate.
-					$exploded = explode('::', $param);
-					$this->populate($model, $exploded, $value);
+				if (method_exists($action, 'getModel')) {
+					// The action supplies an already instantiated model
+					$model = $action->getModel();
 				}
 				else {
-					$model->$param = $this->convertType($value);
+					// The action supplies model class name(s), so we must instantiate
+					$model = $this->instantiateModel($action->model);
+				}
+				
+				if ($model !== null) {
+					foreach ($dispatcher->getRequest() as $param => $value) {
+						if (strpos($param, '::') !== false) {
+							// Parameter is a property path to inner models. Explode the path and populate.
+							$exploded = explode('::', $param);
+							$this->populate($model, $exploded, $value);
+						}
+						else {
+							if (property_exists($model, $param) || $param == 'original') {
+								$model->$param = $this->convertType($value);
+							}
+						}
+					}
+
+					// Prepare a ModelProxy for the model
+					$action->model = Models::getModel($model);
 				}
 			}
-
-			// Prepare a ModelProxy for the model
-			$action->model = Models::getModel($model);
 		}
 
 		return $dispatcher->invoke();
@@ -93,6 +125,8 @@
 	 * of the form <code>outerProperty::innerProperty</code> in which case <code>outerProperty</code> in
 	 * the model must be another model with the an <code>innerProperty</code> property to be populated
 	 * with the value.
+	 * 
+	 * TODO: This must be better documented and possibly add property_exists()-checks
 	 *
 	 * @param object $model		Model object to populate.
 	 * @param string $property	Property to populate.

=== renamed file 'src/interceptors/ValidatorInterceptor.php' => 'src/interceptors/ValidationInterceptor.php'
--- src/interceptors/ValidatorInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/ValidationInterceptor.php	2009-04-08 15:34:28 +0000
@@ -2,66 +2,53 @@
 /**
  * Interceptor handling model validation and its corresponding error messages.
  * 
- * The target action must be <code>ValidationAware</code> and return a fully populated model which
- * we are to validate. This model is usually populated by a <code>ModelInterceptor</code>. If any
- * validation errors occures, error messages are put into the action so the view can display them.
- * 
- * @version		$Id: ValidatorInterceptor.php 1526 2008-07-14 16:07:05Z anders $
+ * The target action must be <code>ValidationAware</code> and have a fully populated model
+ * which we are to validate in a public <code>$model</code> property. This model is usually
+ * populated by a <code>ModelInterceptor</code>. If validation fails, the
+ * <code>validationFailed()</code> method on the action is called for the action to determine
+ * the result and set any custom error message.
  */
-class ValidatorInterceptor extends AbstractInterceptor {
+class ValidationInterceptor extends AbstractInterceptor {
 	/**
 	 * Invokes and processes the interceptor.
 	 */
 	public function intercept ($dispatcher) {
 		$action = $dispatcher->getAction();
+		$valid = true;
 		
-		if ($action instanceof ValidationAware && $dispatcher->getRequest()->getMethod() == 'POST'
-				&& isset($action->model) && is_object($action->model)) {
-			/*
-			 * Action is ValidationAware, request is POSTed and a model is prepared. Create a validator,
-			 * do the validation and put errors into the action.
-			 */
-			$conf = Config::getValidationConfig();
-			$validator = new Validator($conf['classes'], $conf['messages']);
-			$action->errors = $validator->validate($action->model);
-		}
-
-		$result = $dispatcher->invoke();
-
-		if ($action instanceof ValidationAware && $action instanceof MessageAware) {
-			// Report errors if action has any errors registered
-			if (!empty($action->errors)) {
-				$action->msg->setMessage('Submitted form contains errors. Please correct any errors listed
-							in the form and try again.', false);
+		// Validate model if action wants validation and a validateable model is prepared
+		if ($action instanceof ValidationAware
+				&& $action->model instanceof ValidateableModelProxy) {
+			if (!$action->model->validate()) {
+				$valid = false;
+				// Retrieve the result we want to return
+				$result = $action->validationFailed();
+			}
+		}
+
+		if ($valid) {
+			$result = $dispatcher->invoke();
+		}
+		else {
+			/*
+			 * If validationFailed() didn't set a specific message, we give a general
+			 * error message.
+			 */
+			if ($action instanceof MessageAware && $action->msg->isEmpty()) {
+				$action->msg->setMessage('Submitted form contains errors. Please correct
+						any errors listed in the form and try again.', false);
+			}
+
+			/*
+			 * If the result is a redirect and sessions are enabled, store model for
+			 * retrieval after redirect.
+			 */
+			if ($result instanceof RedirectResult && $action instanceof SessionAware) {
+				$action->session['model'] = $action->model;
 			}
 		}
 
 		return $result;
-//		TODO: Do we want to fix this? Errors in session is pretty useless as is below, as the invalid data
-//			isn't present after a redirect. If we do want this, submitted model data should probably be stored.
-//		$this->checkErrorsInSession($action);
-//		return $result;
 	}
-
-	/**
-	 * Checks to see if the session has error messages stored while the action do not. If so, the
-	 * errors are injected into the action and removed from the session.
-	 */
-//	private function checkErrorsInSession ($dispatcher) {
-//		$session = $dispatcher->getRequest()->getSession();
-//
-//		if ($session !== null) {
-//			$action = $dispatcher->getAction();
-//
-//			if ($action instanceof ValidationAware && empty($action->errors)) {
-//				$errorsInSession = $session->get('errors');
-//
-//				if (!empty($errorsInSession)) {
-//					$action->errors = $errorsInSession;
-//					$session->remove('errors');
-//				}
-//			}
-//		}
-//	}
 }
 ?>

=== modified file 'src/models/ModelProxy.php'
--- src/models/ModelProxy.php	2009-02-22 03:44:58 +0000
+++ src/models/ModelProxy.php	2009-04-04 18:03:16 +0000
@@ -42,6 +42,12 @@
 	 * @var object
 	 */
 	protected $current;
+	
+	/**
+	 * Flag to indicate whether we have proxified inner models.
+	 * @var bool
+	 */
+	protected $isInnerProxied;
 
 	/**
 	 * Creates a <code>ModelProxy</code> instance for the model supplied.
@@ -58,65 +64,45 @@
 			$model = current($this->models);
 		}
 
+		$this->isInnerProxied = false;
 		$this->initDaoProxy($model);
 	}
-
+	
 	/**
 	 * Iterates the contained models and updates dirty models or inserts new models. The number of
 	 * actually saved rows in the database is returned.
 	 *
-	 * @return int Number of saved rows in the database.
+	 * @return mixed	Number of saved rows in the database, or <code>false</code> if a
+	 *					preSave() method on a model returned false.
 	 */
 	public function save () {
+		if (!isset($this->objects)) {
+			$type = get_class($this->current);
+			throw new Exception("Model $type is not DataProvided and cannot be saved.");
+		}
+		
+		$this->proxifyInnerModels();
 		$numAffected = 0;
-
-		if (isset($this->objects)) {
-			foreach ($this->models as $idx => $model) {
-				$proceed = true;
-
-				// If model has preSave()-method, call it to determine if we should continue
-				// XXX: Should this be here, or is it more appropriate in DataAccessProxy?
-				if (method_exists($model, 'preSave')) {
-					$proceed = $model->preSave();
-				}
-
-				if ($proceed !== false) {
-					if (!empty($model->original)) {
-						if (property_exists($model, 'isDirty') && $model->isDirty) {
-							$numAffected += $this->objects->update($model);
-						}
-					}
-					else {
-						$numAffected += $this->objects->insert($model);
-					}
-				}
-
-				// Loop through model properties and save any inner models
-				foreach ($model as $property => &$innerModel) {
-					/*
-					 * If $innerModel is an array or object, we try to proxy it. If it succeeds, it is
-					 * indeed one or more models we might want to save below, so we put the proxy back
-					 * into the model object.
-					 */
-					if (is_array($innerModel) || is_object($innerModel)) {
-						$proxy = Models::getModel($innerModel);
-						if ($proxy !== null) {
-							$innerModel = $proxy;
-						}
-					}
-
-					if ($innerModel instanceof ModelProxy) {
-						$this->propagateKey($innerModel, $model);
-						// XXX: We do nothing with the number of affected rows for inner saves. Should we?
-						$innerModel->save();
-					}
-				}
-
-				// The model might have been updated or inserted, so set as not-dirty regardless
-				$model->isDirty = false;
+		
+		foreach ($this->models as $model) {
+			// Checks if this model is approved for processing to continue
+			if (!$this->preSaveModel($model)) {
+				// XXX: Should we return false, or make more noise with an exception?
+				return false;
+			}
+			
+			// Process this model (save and/or validate)
+			$numAffected += $this->saveModel($model);
+			
+			foreach ($model as $property) {
+				if ($property instanceof ModelProxy) {
+					// Propagate primary key into inner model and recurse save
+					$this->propagateKey($innerModel, $model);
+					$property->save();
+				}
 			}
 		}
-
+		
 		return $numAffected;
 	}
 
@@ -152,7 +138,7 @@
 
 		return $numAffected;
 	}
-
+	
 	/**
 	 * Checks if the specified property on the current model is empty.
 	 *
@@ -198,7 +184,7 @@
 			if (property_exists($model, $name)) {
 				if ($model->$name !== $value) {
 					$model->$name = $value;
-					$model->isDirty = true;
+					$this->modelChanged($model);
 				}
 			}
 		}
@@ -214,7 +200,7 @@
 	 */
 	public function __call ($name, $args) {
 		if (!empty($args)) {
-			$this->current->isDirty = true;
+			$this->modelChanged($this->current);
 		}
 
 		$reflection = new ReflectionMethod(get_class($this->current), $name);
@@ -252,7 +238,7 @@
 		if (is_object($value)) {
 			if ($offset !== null) {
 				$this->models[$offset] = $value;
-				$this->models[$offset]->isDirty = true;
+				$this->modelChanged($this->models[$offset]);
 			}
 			else {
 				$this->models[] = $value;
@@ -341,7 +327,57 @@
 		}
 		return $this->models;
 	}
-
+	
+	/**
+	 * Flag the model as dirty, as changes have been made to its state.
+	 *
+	 * @param object $model	The model whose state has changed.
+	 */
+	protected function modelChanged ($model) {
+		$model->isDirty = true;
+	}
+	
+	/**
+	 * Calls any existing <code>preSave()</code> method on the supplied model before
+	 * <code>saveModel()</code> is invoked. This makes it possible for the model itself to hook
+	 * into the save process.
+	 *  
+	 * @param object $model	The model to invoke any preSave() on.
+	 * @return bool			<code>true</code> if we should call saveModel() next,
+	 * 						<code>false</code> if we should stop the saving.
+	 */
+	protected function preSaveModel ($model) {
+		// If model has preSave()-method, call it to determine if it approves saving
+		if (method_exists($model, 'preSave')) {
+			return $model->preSave();
+		}
+		return true;
+	}
+	
+	/**
+	 * Saves the supplied model by calling the <code>update()</code> DAO method if the model isn't
+	 * new and changes have been made on its data, or the <code>insert()</code> DAO method if it's
+	 * new.
+	 *
+	 * @param object $model	The model to save.
+	 * @return int			Number of rows affected in the database.
+	 */
+	protected function saveModel ($model) {
+		$numAffected = 0;
+		
+		if (!empty($model->original)) {
+			if (property_exists($model, 'isDirty') && $model->isDirty) {
+				 $numAffected = $this->objects->update($model);
+			}
+		}
+		else {
+			$numAffected = $this->objects->insert($model);
+		}
+		
+		$model->isDirty = false;
+		return $numAffected;
+	}
+	
 	/**
 	 * Initializes a proxy to the data access object of the model, if it is <code>DataProvided</code>.
 	 *
@@ -352,21 +388,51 @@
 			$this->objects = new DataAccessProxy($this, get_class($model));
 		}
 	}
-
-	/**
-	 * Checks every model object in a ModelProxy for the existance of a foreign key to the supplied model
-	 * and updates it's value if it's empty.
+	
+	/**
+	 * Iterates over the contained models and their properties, and proxifies any inner models.
+	 * This makes it possible for us to automatically validate and save them along with the
+	 * outermost models.
+	 */
+	protected function proxifyInnerModels () {
+		if ($this->isInnerProxied) {
+			return;
+		}
+		
+		foreach ($this->models as $model) {
+			foreach ($model as &$innerModel) {
+				/*
+				 * If $innerModel is an array or object, we try to proxy it. If it succeeds, it is
+				 * indeed one or more models, so set the created proxy back into the model object.
+				 */
+				if (is_array($innerModel) || is_object($innerModel)) {
+					$proxy = Models::getModel($innerModel);
+					if ($proxy !== null) {
+						$innerModel = $proxy;
+						// Recurse to proxify even more deeply nested models
+						$innerModel->proxifyInnerModels();
+					}
+				}
+			}
+		}
+		
+		$this->isInnerProxied = true;
+	}
+	
+	/**
+	 * Checks every model object in a ModelProxy for the existance of a foreign key to the supplied
+	 * model and updates it's value if it's empty.
 	 *
 	 * @param ModelProxy $proxy The ModelProxy with model objects to update.
-	 * @param object $model     The model whose primary key defines the foreign key to look for.
+	 * @param object $model     The model whose primary key defines the foreign key to update.
 	 */
 	private function propagateKey ($proxy, $model) {
 		$reflection = new ReflectionObject($model);
 		$pk = $reflection->getConstant('PK');
 
 		foreach ($proxy as $innerModel) {
+			// Update the foreign key to main model if it exists and is empty
 			if (property_exists($innerModel, $pk) && empty($innerModel->$pk)) {
-				// Inner model has an empty foreign key to the main model, initialize before saving
 				$innerModel->$pk = $model->$pk;
 			}
 		}

=== modified file 'src/models/ValidateableModelProxy.php'
--- src/models/ValidateableModelProxy.php	2008-10-20 16:41:10 +0000
+++ src/models/ValidateableModelProxy.php	2009-04-04 18:03:16 +0000
@@ -1,28 +1,108 @@
 <?php
 /**
- * This class is a validateable model proxy. This model proxy is used for <code>Validateable</code> models in order to
- * correctly expose their functionality.
- *
- * @version		$Id: ValidateableModelProxy.php 1542 2008-08-12 18:46:42Z anders $
+ * This class is a validateable model proxy. This model proxy is used for <code>Validateable</code>
+ * models in order to add support for validation.
  */
-class ValidateableModelProxy extends ModelProxy implements Validateable {
-	/**
-	 * Creates a <code>ValidateableModelProxy</code> instance for the model supplied. It is assumed that
-	 * the model has been verified <code>Validateable</code>.
+class ValidateableModelProxy extends ModelProxy {
+	
+	/**
+	 * @var Validator
+	 */
+	private $validator;
+	
+	/**
+	 * Creates a <code>ValidateableModelProxy</code> instance for the model supplied. It is assumed
+	 * that the model has been verified <code>Validateable</code>.
 	 *
 	 * @param object $model		Model to proxy.
 	 */
-	public function __construct ($model/*, $dirty*/) {
-		parent::__construct($model/*, $dirty*/);
-	}
-
-	/**
-	 * Calls <code>rules()</code> on the current model and returns its result.
+	public function __construct ($model) {
+		parent::__construct($model);
+	}
+	
+	/**
+	 * Overrides ModelProxy::save() by making sure contained models are valid before they
+	 * are saved. Contained models that have already been validated are not validated again.
+	 * 
+	 * @return mixed	Number of saved rows in the database, or <code>false</code> if a
+	 *					a model contains invalid data or a preSave() method on a model returned
+	 *					false.
+	 * 
+	 */
+	public function save () {
+		if (!$this->validate()) {
+			return false;
+		}
+		return parent::save();
+	}
+	
+	/**
+	 * Validates the contained models and returns <code>true</code> if all are valid or
+	 * <code>false</code> if one or more are invalid.
+	 * 
+	 * @return bool		<code>true</code> if all models are valid, <code>false</code> if not.
+	 */
+	public function validate () {
+		$this->proxifyInnerModels();
+		$this->initValidator();
+		$isValid = true; // We start out with valid state
+		
+		foreach ($this->models as $model) {
+			// And set invalid for invalid objects, but never again valid
+			$isValid = ($this->validateModel($model) ? $isValid : false);
+			foreach ($model as $property) {
+				if ($property instanceof ValidateableModelProxy) {
+					// Recurse to validate inner models
+					$property->validate();
+				}
+			}
+		}
+		
+		return $isValid;
+	}
+	
+	/**
+	 * Alias of validate() to accommodate for more readable code (i.e. for tests).
+	 * 
+	 * @return bool
+	 */
+	public function isValid () {
+		return $this->validate();
+	}
+	
+	/**
+	 * Validates the supplied model. If the model has already been validated (and is unchanged
+	 * since) its previous result is returned, else the Validator is invoked to validate the model.
+	 * 
+	 * @param object $model	The model to validate.
+	 * @return bool
+	 */
+	protected function validateModel ($model) {
+		if (property_exists($model, 'isValid')) {
+			return $model->isValid;
+		}
+		return $this->validator->validate($model);
+	}
+	
+	/**
+	 * Remove validated flag, as changes have been made to its state and it's unknown whether it
+	 * is valid or not.
 	 *
-	 * @return array	Validation rules for the current model.
-	 */
-	public function rules () {
-		return $this->current->rules();
+	 * @param object $model	The model whose state has changed.
+	 */
+	protected function modelChanged ($model) {
+		parent::modelChanged($model);
+		unset($model->isValid);
+	}
+	
+	/**
+	 * Initialized the validator if not already initialized.
+	 */
+	private function initValidator () {
+		if (!isset($this->validator)) {
+			$conf = Config::getValidationConfig();
+			$this->validator = new Validator($conf['classes'], $conf['messages']);
+		}
 	}
 }
 ?>

=== modified file 'src/results/RedirectResult.php'
--- src/results/RedirectResult.php	2008-12-17 15:50:47 +0000
+++ src/results/RedirectResult.php	2009-04-07 23:17:37 +0000
@@ -1,44 +1,29 @@
 <?php
 /**
- * Provides the implementation of a result set which when rendered sends a redirect to the client.
+ * Provides the implementation of a result set which when rendered sends a redirect to the
+ * client. It defaults to a 303 (See Other) status code, but this can be overridden.
  */	
 class RedirectResult extends AbstractResult {
 	private $location;
+	private $code;
 
 	/**
 	 * Constructor.
 	 * 
 	 * @param string $location Location of the redirect relative to the web root.
+	 * @param int $code        HTTP status code to use. Defaults to 303.
 	 */
-	public function __construct ($action, $location) {
+	public function __construct ($action, $location, $code = 303) {
 		parent::__construct($action);
 		$this->location = Config::get('webRoot') . $location;
+		$this->code = $code;
 	}
 
 	/**
 	 * Sends the redirect to the client.
 	 */
 	public function render ($request) {
-		$action = $this->getAction();
-
-		/*
-		 * If a session is active and the action has a message, store them temporarily in the
-		 * session through the redirect.
-		 */
-		if ($action instanceof SessionAware) {
-
-			if ($action instanceof MessageAware && !$action->msg->isEmpty()) {
-				$action->session['message'] = $action->msg;
-			}
-			// See ValidationInterceptor for reason this is commented away
-			//if ($action instanceof ValidationAware && !empty($action->errors)) {
-			//	$session->put('errors', $action->errors);
-			//}
-
-			$action->session->write();
-		}
-
-		header("Location: $this->location");
+		header("Location: $this->location", true, $this->code);
 		exit;
 	}
 }

=== modified file 'src/validation/Validator.php'
--- src/validation/Validator.php	2008-10-20 16:41:10 +0000
+++ src/validation/Validator.php	2009-04-03 22:25:13 +0000
@@ -34,12 +34,15 @@
 	}
 
 	/**
-	 * Validates a model and returns a two-dimensional array with the specifics of any failures.
-	 * The array keys refer to the property of the model whose validation failed, while the value is
-	 * an array containing human-readable messages of the specifics.
+	 * Validates a model and returns <code>true</code> if the model validates, <code>false</code>
+	 * if not.
+	 * 
+	 * Any validation errors are put into a two-dimensional array with the specifics and set in
+	 * $errors on the model. The array keys refer to the property of the model whose validation
+	 * failed, while the value is an array containing human-readable messages of the specifics.
 	 *
-	 * @param object $model			The model to validate.
-	 * @return array				Specifying any validation errors, or empty if none.
+	 * @param object $model		The model to validate.
+	 * @return bool				Indicating success or failure.
 	 */
 	public function validate ($model) {
 		$ruleSet	= $model->rules();
@@ -74,8 +77,14 @@
 				}
 			}
 		}
-
-		return $errors;
+		
+		if (!empty($errors)) {
+			$model->errors = $errors;
+			$model->isValid = false;
+		}
+		else $model->isValid = true;
+		
+		return $model->isValid;
 	}
 
 	/**

=== modified file 'src/views/snippets/kolibri.xsl'
--- src/views/snippets/kolibri.xsl	2009-04-07 21:01:00 +0000
+++ src/views/snippets/kolibri.xsl	2009-04-07 22:09:03 +0000
@@ -15,9 +15,8 @@
 	<xsl:include href="message.xsl" />
 	<xsl:include href="forms.xsl" />
 	
-	<!-- Makes the XML structures for model and errors available for the custom element templates -->
+	<!-- Makes the XML structures for model available for the custom element templates -->
 	<xsl:variable name="model" select="/result/model" />
-	<xsl:variable name="errors" select="/result/errors" />
 		
 	<func:function name="k:number">
 		<xsl:param name="value" />
@@ -174,7 +173,7 @@
 			<xsl:value-of disable-output-escaping="yes" select="$errors/*[name() = $attribute]" />
 		</xsl:variable-->
 
-		<func:result select="$errors/*[name() = $attribute]" />
+		<func:result select="$model/errors/*[name() = $attribute]" />
 	</func:function>
 	
 	<!--
@@ -194,7 +193,7 @@
 			</xsl:choose>
 		</xsl:param>
 			
-		<func:result select="boolean($errors/*[name() = $attribute])" />
+		<func:result select="boolean($model/errors/*[name() = $attribute])" />
 	</func:function>
 	
 	<!--


Follow ups