Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7795)
@@ -18,24 +18,23 @@
 """Interfaces of the university application package.
 """
-
 from grokcore.content.interfaces import IContainer
-
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
 from zope import schema
-from zope.interface import Interface, Attribute, implements, directlyProvides
 from zope.component import getUtilitiesFor, queryUtility, getUtility
 from zope.catalog.interfaces import ICatalog
+from zope.interface import Interface, Attribute, implements, directlyProvides
 from zope.schema.interfaces import (
     ValidationError, ISource, IContextSourceBinder)
-from zc.sourcefactory.basic import BasicSourceFactory
-from zc.sourcefactory.contextual import BasicContextualSourceFactory
 from waeup.sirp.schema import TextLineChoice
 from waeup.sirp.interfaces import (
     ISIRPObject, year_range, validate_email, academic_sessions_vocab)
 from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.payments.interfaces import IOnlinePayment
+from waeup.sirp.schoolgrades import ResultEntryField
+from waeup.sirp.students.vocabularies import (
+    lgas_vocab, CertificateSource, GenderSource)
 from waeup.sirp.university.vocabularies import (
     course_levels, AppCatSource)
-from waeup.sirp.students.vocabularies import (
-    lgas_vocab, CertificateSource, GenderSource)
-from waeup.sirp.payments.interfaces import IOnlinePayment
 
 #: Maximum upload size for applicant passport photographs (in bytes)
@@ -77,4 +76,5 @@
     return source
 directlyProvides(contextual_reg_num_source, IContextSourceBinder)
+
 
 class AppCatCertificateSource(CertificateSource):
@@ -360,9 +360,9 @@
     state, depending on use-case.
 
-    This base interface is also implemented by the StudentApplication
-    class in the students package. Thus, these are the data which are saved
-    after admission.
-    """
-
+    This base interface is also implemented by the
+    :class:`waeup.sirp.students.StudentApplication` class in the
+    students package. Thus, these are the data which are saved after
+    admission.
+    """
     applicant_id = schema.TextLine(
         title = _(u'Applicant Id'),
@@ -430,4 +430,10 @@
         required = False,
         )
+    school_grades = schema.List(
+        title = _(u'School Grades'),
+        value_type = ResultEntryField(),
+        required = True,
+        default = [],
+        )
 
     #
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7795)
@@ -752,5 +752,36 @@
         self.browser.open(self.edit_path)
         self.assertTrue(
-            '<option selected="selected" value="CERT1">' in self.browser.contents)
+            '<option selected="selected" value="CERT1">'
+            in self.browser.contents)
+
+    def test_school_grades(self):
+        # we can add school grades
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # Fill other fields
+        # add a new (empty) row with subject/grade
+        self.browser.getControl(name="form.school_grades.add").click()
+        # pick first subject (after <no value>)...
+        ctrl_subj = self.browser.getControl(
+            name="form.school_grades.0.subject")
+        ctrl_subj.value = [ctrl_subj.options[1]]
+        display_subj = ctrl_subj.displayOptions[1]
+        # pick first grade (after <no value>)...
+        ctrl_grade = self.browser.getControl(
+            name="form.school_grades.0.grade")
+        ctrl_grade.value = [ctrl_grade.options[1]]
+        display_grade = ctrl_grade.displayOptions[1]
+        # save everything
+        self.browser.getControl("Save").click()
+        self.assertTrue(
+            '<input type="hidden" name="form.school_grades.count" value="1" />'
+            in self.browser.contents)
+        # we can also see the new subject/grade in display view
+        self.browser.open(self.view_path)
+        self.assertTrue(
+            display_subj in self.browser.contents)
+        self.assertTrue(
+            display_grade in self.browser.contents)
+        return
 
 class ApplicantRegisterTests(ApplicantsFullSetup):
Index: main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 7795)
@@ -31,4 +31,5 @@
 from zope.container.interfaces import INameChooser
 from zope.interface import Interface, Attribute, implements
+from zope.schema.interfaces import IObject
 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
 
@@ -79,4 +80,26 @@
     )
 
+SUBJECTS = dict(math=_(u'Math'),
+                bio=_(u'Biology'),
+                eng=_(u'English'),
+                )
+class SubjectSource(BasicSourceFactory):
+    def getValues(self):
+        return sorted(SUBJECTS.keys())
+    def getTitle(self, value):
+        return SUBJECTS.get(value, None)
+
+GRADES = {'A':_(u'Very Good'),
+          'B':_(u'Good'),
+          'C':_(u'Satisfactory'),
+          'D':_(u'Sufficient'),
+          'E':_(u'Fail'),
+          }
+class GradeSource(BasicSourceFactory):
+    def getValues(self):
+        return sorted(GRADES.keys())
+    def getTitle(self, value):
+        return GRADES.get(value, None)
+
 # Define a valiation method for email addresses
 class NotAnEmailAddress(schema.ValidationError):
@@ -145,4 +168,23 @@
         return value
 
+class IResultEntry(Interface):
+    """A school grade entry.
+    """
+    subject = schema.Choice(
+        title = _(u'Subject'),
+        source = SubjectSource(),
+        )
+    grade = schema.Choice(
+        title = _(u'Grade'),
+        source = GradeSource(),
+        )
+
+class IResultEntryField(IObject):
+    """A zope.schema-like field for usage in interfaces.
+
+    Marker interface to distuingish result entries from ordinary
+    object fields. Needed for registration of widgets.
+    """
+
 class ISIRPUtils(Interface):
     """A collection of methods which are subject to customization.
@@ -970,3 +1012,2 @@
         required = True,
         )
-
Index: main/waeup.sirp/trunk/src/waeup/sirp/schoolgrades.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/schoolgrades.py	(revision 7795)
+++ main/waeup.sirp/trunk/src/waeup/sirp/schoolgrades.py	(revision 7795)
@@ -0,0 +1,64 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components representing and aggregating school grades.
+"""
+import grok
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema import Object
+from waeup.sirp.interfaces import IResultEntry, IResultEntryField
+from waeup.sirp.widgets.objectwidget import (
+    SIRPObjectWidget, SIRPObjectDisplayWidget
+    )
+
+class ResultEntry(grok.Model):
+    """A result entry contains a subject and a grade.
+    """
+    grok.implements(IResultEntry)
+    subject = FieldProperty(IResultEntry['subject'])
+    grade = FieldProperty(IResultEntry['grade'])
+
+class ResultEntryField(Object):
+    """A zope.schema-like field for usage in interfaces.
+
+    If you want to define an interface containing result entries, you
+    can do so like this::
+
+      class IMyInterface(Interface):
+          my_result_entry = ResultEntryField()
+
+    Default widgets are registered to render result entry fields.
+    """
+    grok.implements(IResultEntryField)
+
+    def __init__(self, **kw):
+        super(ResultEntryField, self).__init__(IResultEntry, **kw)
+        return
+
+# register SIRPObjectWidgets as default widgets for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def result_entry_input_widget(obj, req):
+    return SIRPObjectWidget(obj, req, ResultEntry)
+
+# register a display widget for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def result_entry_display_widget(obj, req):
+    return SIRPObjectDisplayWidget(obj, req, ResultEntry)
Index: main/waeup.sirp/trunk/src/waeup/sirp/tests/test_schoolgrades.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/tests/test_schoolgrades.py	(revision 7795)
+++ main/waeup.sirp/trunk/src/waeup/sirp/tests/test_schoolgrades.py	(revision 7795)
@@ -0,0 +1,23 @@
+# Tests for schoolgrades module.
+import unittest
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.sirp.interfaces import IResultEntry, IResultEntryField
+from waeup.sirp.schoolgrades import ResultEntry, ResultEntryField
+
+class ResultEntryTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntry()
+        verifyObject(IResultEntry, obj)
+        verifyClass(IResultEntry, ResultEntry)
+        return
+
+class ResultEntryFieldTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntryField()
+        verifyObject(IResultEntryField, obj)
+        verifyClass(IResultEntryField, ResultEntryField)
+        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.pt	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.pt	(revision 7795)
@@ -1,5 +1,7 @@
-  <td class="row" tal:repeat="widget context/subwidgets">
-    <span tal:define="error widget/error"
-	  tal:replace="structure error" tal:condition="error" />
-    <span tal:replace="structure widget" />
-  </td>
+  <div class="row">
+    <span tal:repeat="widget context/subwidgets">
+      <span tal:define="error widget/error"
+	    tal:replace="structure error" tal:condition="error" />
+      <span tal:replace="structure widget" />
+    </span>
+  </div>
Index: main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.py	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/widgets/objectwidget.py	(revision 7795)
@@ -60,4 +60,5 @@
         return SIRPObjectWidgetView(self, request)
 
+
 class SIRPObjectDisplayWidget(SIRPObjectWidget):
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.pt	(revision 7795)
+++ main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.pt	(revision 7795)
@@ -0,0 +1,29 @@
+<table border="0" class="sequencewidget"
+  i18n:domain="zope">
+  <tr tal:repeat="widget view/widgets">
+    <td>
+      <input class="editcheck" type="checkbox"
+             tal:attributes="
+                 name string:${view/name}.remove_${repeat/widget/index}"
+             tal:condition="view/need_delete" />
+    </td>
+    <td>
+      <span tal:define="error widget/error"
+            tal:replace="structure error" tal:condition="error" />
+      <input tal:replace="structure widget" />
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+      <input type="submit" value="Remove selected items"
+             tal:condition="view/need_delete"
+             tal:attributes="name string:${view/name}.remove"
+             i18n:attributes="value remove-selected-items" />
+      <input type="submit" value="Add foo"
+             tal:condition="view/need_add"
+             tal:attributes="name string:${view/name}.add;
+                             value view/addButtonLabel" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.py	(revision 7795)
+++ main/waeup.sirp/trunk/src/waeup/sirp/widgets/sequencewidget.py	(revision 7795)
@@ -0,0 +1,43 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""An improved sequence widget.
+
+We provide a sequence widget that better suits our needs layout-wise
+than the default implementation from zope.formlib.
+"""
+import grok
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.interfaces import IInputWidget
+from zope.formlib.widgets import ListSequenceWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.interfaces import IField, IList
+
+class SIRPSequenceWidget(ListSequenceWidget):
+    """A sequence widget for lists.
+
+    This is basically a plain copy from zope.formlib. We have,
+    however, the possibility to tweak the attached template.
+    """
+    template = ViewPageTemplateFile('sequencewidget.pt')
+
+
+# Register our sequence widget as default for lists.
+@grok.adapter(IList, IField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def seq_input_widget(obj, field, req, *args, **kw):
+    return SIRPSequenceWidget(obj, field, req, *args, **kw)
Index: main/waeup.sirp/trunk/src/waeup/sirp/widgets/tests/test_objectwidget.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 7757)
+++ main/waeup.sirp/trunk/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 7795)
@@ -21,5 +21,4 @@
 import unittest
 import sys
-from zope.component import testing
 from zope.interface import Interface, implements
 from zope.interface.verify import verifyClass
@@ -45,5 +44,5 @@
     name = TextLine()
     email = TextLine()
-    
+
 class TestContact(object):
     implements(ITestContact)
@@ -51,9 +50,9 @@
 class ITestContent(Interface):
     foo = Object(
-        ITestContact, 
+        ITestContact,
         title = u'Foo Title',
         description = u'',
         )
-    
+
 class ObjectWidgetInputErrorView(object):
     implements(IWidgetInputErrorView)
@@ -82,5 +81,5 @@
         provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
                        IInputWidget)
-        
+
         class TestObject(object):
             implements(ITestContent)
@@ -120,5 +119,5 @@
         error_html = widget.error()
         if sys.version_info < (2, 5):
-            self.assertTrue("email: <zope.formlib.interfaces.Mis" 
+            self.assertTrue("email: <zope.formlib.interfaces.Mis"
                              in error_html)
             self.assertTrue("name: <zope.formlib.interfaces.Miss"
@@ -161,6 +160,6 @@
         widget1, widget2 = widget.subwidgets()
         self.assertTrue(widget1 is name_widget)
-        
-        
+
+
 class ObjectDisplayWidgetTest(BrowserWidgetTest):
     """Documents and tests the display variant of object widget.
@@ -184,5 +183,5 @@
         # adapter that can turn nearly any object into an ITraversable.
         provideAdapter(DefaultTraversable, (None,), ITraversable)
-        
+
         class TestObject(object):
             implements(ITestContent)
@@ -206,7 +205,6 @@
                        IDisplayWidget)
 
-        
+
     def test_interfaces(self):
-        from zope.interface.verify import DoesNotImplement
         self.assertTrue(IDisplayWidget.providedBy(self._widget))
         self.assertFalse(IInputWidget.providedBy(self._widget))
@@ -219,10 +217,11 @@
 
         check_list = [
-            '<td', 'class=', 'row',
+            '<div', 'class=', 'row',
+            '<span',
             'Foo Name',
-            '</td>', '<td', 'class=', 'row',
+            '</span>', '<span',
             'foo@foo.test',
-            '</td>']
-        self.verifyResult(widget(), check_list, inorder=True)    
+            '</div>']
+        self.verifyResult(widget(), check_list, inorder=True)
 
 def test_suite():
