Index: /trunk/camelot/admin/validator/object_validator.py
===================================================================
--- /trunk/camelot/admin/validator/object_validator.py (revision 1060)
+++ /trunk/camelot/admin/validator/object_validator.py (revision 1186)
@@ -46,5 +46,34 @@
         empty list if object is valid
         """
-        return []
+        from camelot.view.controls import delegates
+        messages = []
+        fields_and_attributes = dict(self.admin.get_columns())
+        fields_and_attributes.update(dict(self.admin.get_fields()))
+        for field, attributes in fields_and_attributes.items():
+            # if the field was not editable, don't waste any time
+            if attributes['editable']:
+              value = getattr(entity_instance, field)
+              #@todo: check if field is a primary key instead of checking 
+              # whether the name is id, but this should only happen in the entity validator
+              if attributes['nullable']!=True and field!='id':
+                  logger.debug('column %s is required'%(field))
+                  if 'delegate' not in attributes:
+                      raise Exception('no delegate specified for %s'%(field))
+                  is_null = False
+                  if value==None:
+                      is_null = True
+                  elif (attributes['delegate'] == delegates.CodeDelegate) and \
+                       (sum(len(c) for c in value) == 0):
+                      is_null = True
+                  elif (attributes['delegate'] == delegates.PlainTextDelegate) and (len(value) == 0):
+                      is_null = True
+                  elif (attributes['delegate'] == delegates.Many2OneDelegate) and (not value.id):
+                      is_null = True
+                  elif (attributes['delegate'] == delegates.VirtualAddressDelegate) and (not value[1]):
+                      is_null = True                    
+                  if is_null:
+                      messages.append(u'%s is a required field' % (attributes['name']))
+        logger.debug(u'messages : %s'%(u','.join(messages)))
+        return messages
 
     def isValid(self, row):
Index: /trunk/camelot/admin/validator/entity_validator.py
===================================================================
--- /trunk/camelot/admin/validator/entity_validator.py (revision 987)
+++ /trunk/camelot/admin/validator/entity_validator.py (revision 1186)
@@ -37,36 +37,3 @@
     """
 
-    def objectValidity(self, entity_instance):
-        """:return: list of messages explaining invalid data
-        empty list if object is valid
-        """
-        from camelot.view.controls import delegates
-        messages = []
-        fields_and_attributes = dict(self.admin.get_columns())
-        fields_and_attributes.update(dict(self.admin.get_fields()))
-        for field, attributes in fields_and_attributes.items():
-            # if the field was not editable, don't waste any time
-            if attributes['editable']:
-              value = getattr(entity_instance, field)
-              #@todo: check if field is a primary key instead of checking 
-              # whether the name is id
-              if attributes['nullable']!=True and field!='id':
-                  logger.debug('column %s is required'%(field))
-                  if 'delegate' not in attributes:
-                      raise Exception('no delegate specified for %s'%(field))
-                  is_null = False
-                  if value==None:
-                      is_null = True
-                  elif (attributes['delegate'] == delegates.CodeDelegate) and \
-                       (sum(len(c) for c in value) == 0):
-                      is_null = True
-                  elif (attributes['delegate'] == delegates.PlainTextDelegate) and (len(value) == 0):
-                      is_null = True
-                  elif (attributes['delegate'] == delegates.Many2OneDelegate) and (not value.id):
-                      is_null = True
-                  elif (attributes['delegate'] == delegates.VirtualAddressDelegate) and (not value[1]):
-                      is_null = True                    
-                  if is_null:
-                      messages.append(u'%s is a required field' % (attributes['name']))
-        logger.debug(u'messages : %s'%(u','.join(messages)))
-        return messages
+
Index: /trunk/camelot/view/field_attributes.py
===================================================================
--- /trunk/camelot/view/field_attributes.py (revision 1179)
+++ /trunk/camelot/view/field_attributes.py (revision 1186)
@@ -31,4 +31,5 @@
 import camelot.types
 import datetime
+import operator
 
 from controls import delegates
@@ -45,4 +46,5 @@
 )
 
+_numerical_operators = (operator.eq, operator.lt, operator.le, operator.gt, operator.ge)
 
 _sqlalchemy_to_python_type_ = {
@@ -53,5 +55,6 @@
         'nullable': True,
         'delegate': delegates.BoolDelegate,
-        'from_string': bool_from_string
+        'from_string': bool_from_string,
+        'operators' : (operator.eq,),
     },
 
@@ -61,5 +64,6 @@
         'nullable': True,
         'delegate': delegates.BoolDelegate,
-        'from_string': bool_from_string
+        'from_string': bool_from_string,
+        'operators' : (operator.eq,),
     },
 
@@ -72,5 +76,6 @@
         'nullable': True,
         'delegate': delegates.DateDelegate,
-        'from_string': date_from_string
+        'from_string': date_from_string,
+        'operators' : _numerical_operators,
     },
 
@@ -83,5 +88,6 @@
             'format': constants.camelot_time_format,
             'nullable': True,
-            'from_string': time_from_string
+            'from_string': time_from_string,
+            'operators': _numerical_operators,
     },
 
@@ -94,5 +100,6 @@
         'nullable': True,
         'delegate': delegates.DateTimeDelegate,
-        'from_string': datetime_from_string
+        'from_string': datetime_from_string,
+        'operators': _numerical_operators,
     },
 
@@ -105,5 +112,6 @@
         'nullable': True,
         'delegate': delegates.FloatDelegate,
-        'from_string': float_from_string
+        'from_string': float_from_string,
+        'operators': _numerical_operators,
     },
 
@@ -117,5 +125,6 @@
         'from_string': int_from_string,
         'to_string': unicode,
-        'widget': 'int'
+        'widget': 'int',
+        'operators': _numerical_operators,
     },
 
@@ -128,5 +137,6 @@
         'delegate': delegates.IntegerDelegate,
         'from_string': int_from_string,
-        'widget': 'int'
+        'widget': 'int',
+        'operators': _numerical_operators,
     },
 
Index: /trunk/camelot/view/proxy/collection_proxy.py
===================================================================
--- /trunk/camelot/view/proxy/collection_proxy.py (revision 1174)
+++ /trunk/camelot/view/proxy/collection_proxy.py (revision 1186)
@@ -259,4 +259,7 @@
         self.logger.debug( 'initialization finished' )
 
+    def get_validator(self):
+        return self.validator
+    
     def map_to_source(self, sorted_row_number):
         """Converts a sorted row number to a row number of the source
Index: /trunk/camelot/view/proxy/queryproxy.py
===================================================================
--- /trunk/camelot/view/proxy/queryproxy.py (revision 1170)
+++ /trunk/camelot/view/proxy/queryproxy.py (revision 1186)
@@ -33,8 +33,6 @@
 logger = logging.getLogger('camelot.view.proxy.queryproxy')
 
-from collection_proxy import CollectionProxy, stripped_data_to_unicode, \
-                             strip_data_from_object, tool_tips_from_object, \
-                             background_colors_from_object
-from camelot.view.model_thread import model_function, gui_function
+from collection_proxy import CollectionProxy, strip_data_from_object
+from camelot.view.model_thread import model_function, gui_function, post
 
 
@@ -50,4 +48,5 @@
         logger.debug('initialize query table')
         self._query_getter = query_getter
+        self._sort_decorator = None
         #rows appended to the table which have not yet been flushed to the
         #database, and as such cannot be a result of the query
@@ -56,4 +55,14 @@
                                  columns_getter, max_number_of_rows=10, edits=None)
 
+    def get_query_getter(self):
+        if not self._sort_decorator:
+            return self._query_getter
+        else:
+            
+            def sorted_query_getter():
+                return self._sort_decorator(self._query_getter())
+            
+            return sorted_query_getter
+    
     @model_function
     def _clean_appended_rows(self):
@@ -69,5 +78,5 @@
     def getRowCount(self):
         self._clean_appended_rows()
-        return self._query_getter().count() + len(self._appended_rows)
+        return self.get_query_getter()().count() + len(self._appended_rows)
 
     @gui_function
@@ -80,5 +89,5 @@
         
         def collection_getter():
-            return self._query_getter().all()
+            return self.get_query_getter()().all()
         
         return collection_getter
@@ -86,5 +95,42 @@
     @gui_function
     def sort( self, column, order ):
-        pass   
+        
+        def create_set_sort_decorator(column, order):
+
+            def set_sort_decorator():
+                from sqlalchemy import orm
+                from sqlalchemy.exceptions import InvalidRequestError
+                field_name = self._columns[column][0]
+                class_attribute = getattr(self.admin.entity, field_name)
+                mapper = orm.class_mapper(self.admin.entity)
+                try:
+                    property = mapper.get_property(
+                        field_name,
+                        resolve_synonyms=True
+                    )
+                except InvalidRequestError:
+                    #
+                    # If the field name is not a property of the mapper, we cannot
+                    # sort it using sql
+                    #
+                    return self.rows
+                
+                def create_sort_decorator(class_attribute, order):
+                    
+                    def sort_decorator(query):
+                        if order:
+                            return query.order_by(class_attribute.desc())
+                        else:
+                            return query.order_by(class_attribute)
+                    
+                    return sort_decorator
+                
+                
+                self._sort_decorator = create_sort_decorator(class_attribute, order)
+                return self.rows
+                    
+            return set_sort_decorator
+            
+        post( create_set_sort_decorator(column, order), self._refresh_content )
 
     def append(self, o):
@@ -103,5 +149,5 @@
     def getData(self):
         """Generator for all the data queried by this proxy"""
-        for _i,o in enumerate(self._query_getter().all()):
+        for _i,o in enumerate(self.get_query_getter()().all()):
             yield strip_data_from_object(o, self.getColumns())
 
@@ -109,5 +155,5 @@
     def _extend_cache(self, offset, limit):
         """Extend the cache around row"""
-        q = self._query_getter().offset(offset).limit(limit)
+        q = self.get_query_getter()().offset(offset).limit(limit)
         columns = self.getColumns()
         for i, o in enumerate(q.all()):
@@ -136,5 +182,5 @@
                 pass
             # momentary hack for list error that prevents forms to be closed
-            res = self._query_getter().offset(row)
+            res = self.get_query_getter()().offset(row)
             if isinstance(res, list):
                 res = res[0]
Index: /trunk/camelot/view/elixir_admin.py
===================================================================
--- /trunk/camelot/view/elixir_admin.py (revision 1146)
+++ /trunk/camelot/view/elixir_admin.py (revision 1186)
@@ -169,5 +169,5 @@
                 from elixir import entities
                 mapped_entities = [str(e) for e in entities]
-                logger.error(u'%s is not a mapped class, mapped classes include %s'%(self.entity, u','.join(mapped_entities)),
+                logger.error(u'%s is not a mapped class, mapped classes include %s'%(self.entity, u','.join([unicode(me) for me in mapped_entities])),
                              exc_info=exception)
                 raise exception
@@ -401,5 +401,5 @@
                 model = QueryTableProxy(
                     tableview.admin,
-                    tableview._table_model._query_getter,
+                    tableview._table_model.get_query_getter(),
                     tableview.admin.get_fields,
                     max_number_of_rows=1
Index: /trunk/camelot/view/wizard/pages/form_page.py
===================================================================
--- /trunk/camelot/view/wizard/pages/form_page.py (revision 1185)
+++ /trunk/camelot/view/wizard/pages/form_page.py (revision 1186)
@@ -26,8 +26,9 @@
 #  ============================================================================
 
-from PyQt4 import QtGui
+from PyQt4 import QtGui, QtCore
 
 from camelot.core.utils import ugettext as _
 from camelot.view.art import Icon
+from camelot.view.model_thread import post
 
 class FormPage(QtGui.QWizardPage):
@@ -37,5 +38,7 @@
     
     To access the data stored by the wizard form into a data object, use its
-    get_data method .
+    get_data method.
+    
+    The 'Next' button will only be activated when the form is complete.
     """
 
@@ -57,14 +60,30 @@
         self.setPixmap(QtGui.QWizard.LogoPixmap, self.get_icon().getQPixmap())
         self._data = self.Data()
+        self._complete = False
         
         admin = self.get_admin()
-        collection_proxy = CollectionProxy(admin, lambda:[self._data], admin.get_fields)
+        self._model = CollectionProxy(admin, lambda:[self._data], admin.get_fields)
         
         layout = QtGui.QVBoxLayout()
         form = FormWidget(admin)
-        form.set_model(collection_proxy)
+        self.connect(form, FormWidget.changed_signal, self._form_changed)
+        form.set_model(self._model)
         layout.addWidget(form)
         self.setLayout(layout)
+    
+    def _form_changed(self):
+        
+        def is_valid():
+            return self._model.get_validator().isValid(0)
+        
+        post(is_valid, self._change_complete)
+        
+    def _change_complete(self, complete):
+        self._complete = complete
+        self.emit(QtCore.SIGNAL('completeChanged ()'))
             
+    def isComplete(self):
+        return self._complete
+    
     def get_admin(self):
         from camelot.view.application_admin import get_application_admin
Index: /trunk/camelot/view/controls/formview.py
===================================================================
--- /trunk/camelot/view/controls/formview.py (revision 1185)
+++ /trunk/camelot/view/controls/formview.py (revision 1186)
@@ -55,4 +55,7 @@
         self.setLayout( self._widget_layout )
                 
+    def get_model(self):
+        return self._model
+    
     def set_model(self, model):
         self._model = model
Index: /trunk/camelot/view/controls/tableview.py
===================================================================
--- /trunk/camelot/view/controls/tableview.py (revision 1184)
+++ /trunk/camelot/view/controls/tableview.py (revision 1186)
@@ -127,5 +127,5 @@
             self.number_of_rows = None
         layout.addLayout( widget_layout )
-        self._expanded_search = QtGui.QLabel('Expanded')
+        self._expanded_search = QtGui.QWidget()
         self._expanded_search.hide()
         layout.addWidget(self._expanded_search)
@@ -133,5 +133,15 @@
         self.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Fixed )
         self.setNumberOfRows( 0 )
-    
+        post( admin.get_columns, self._fill_expanded_search_options )
+    
+    def _fill_expanded_search_options(self, columns):
+        layout = QtGui.QHBoxLayout()
+        for field, attributes in columns:
+            if 'operators' in attributes:
+                widget = QtGui.QLabel(field)
+                layout.addWidget( widget )
+        self._expanded_search.setLayout( layout )
+        
+        
     def expand_search_options(self):
         if self._expanded_search.isHidden():
