Index: /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/schoolgrades.py
===================================================================
--- /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/schoolgrades.py	(revision 12570)
+++ /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/schoolgrades.py	(revision 12570)
@@ -0,0 +1,315 @@
+## $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 zc.sourcefactory.basic import BasicSourceFactory
+from zope.interface import Interface
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope import schema
+from zope.schema.interfaces import IObject
+from zope.schema.fieldproperty import FieldProperty
+from waeup.ikoba.widgets.objectwidget import (
+    IkobaObjectWidget, IkobaObjectDisplayWidget
+    )
+from ikobacustom.pcn.interfaces import MessageFactory as _
+
+EXAM_SUBJECTS_DICT = {
+    'accounts': 'Accounts',
+    'add_general_science': 'Add General Science',
+    'add_maths': 'Add Maths',
+    'adv_elect_install_maint': 'Adv. Electrical Installation and Maintenance',
+    'agricultural_science': 'Agricultural Science',
+    'applied_electricity': 'Applied Electricity',
+    'arabi': 'Arabic',
+    'arithmetics': 'Arithmetics',
+    'art': 'Art',
+    'auto_mechanics': 'Auto Mechanics',
+    'awaiting_results': 'Awaiting Results',
+    'basic_electricity': 'Basic Electricity',
+    'bible_knowledge': 'Bible Knowledge',
+    'biology': 'Biology',
+    'biology_alt_syl': 'Biology (Alt.Syl)',
+    'book_keeping': 'Book Keeping',
+    'building_construction': 'Building Construction',
+    'business_management': 'Business Management',
+    'business_construction_management': 'Business/Construction Management (Adv)',
+    'chemistry': 'Chemistry',
+    'chemistry_alt_syl': 'Chemistry (Alt.Syl)',
+    'christian_religious_studies': 'Christian Religious Studies',
+    'class_teaching': 'Class Teaching',
+    'clerical_office_duties': 'Clerical Office Duties',
+    'clothing_and_textiles': 'Clothing and Textiles',
+    'commerce': 'Commerce',
+    'economics': 'Economics',
+    'education': 'Education',
+    'efik': 'Efik',
+    'electronics': 'Electronics',
+    'elementary_surveying': 'Elementary Surveying',
+    'english_language': 'English Language',
+    'engineering_science': 'Engineering Science',
+    'financial_accounting': 'Financial Accounting',
+    'food_and_nutrition': 'Food and Nutrition',
+    'french': 'French',
+    'further_mathematics': 'Further Mathematics',
+    'general_science': 'General Science',
+    'geography': 'Geography',
+    'german': 'German',
+    'government': 'Government',
+    'hausa': 'Hausa',
+    'hausa_literature': 'Hausa Literature',
+    'health_science': 'Health Science',
+    'history': 'History',
+    'home_management': 'Home Management',
+    'human_biology': 'Human Biology',
+    'industrial_electrical_installation': 'Industrial Elect Installation (Adv)',
+    'intergrated_science': 'Intergrated Science',
+    'islamic_studies': 'Islamic Studies',
+    'literature_in_english': 'Literature in English',
+    'literature_in_nigerian_languages': 'Literature in Nigerian Languages',
+    'igbo': 'Igbo',
+    'igbo_literature': 'Igbo Literature',
+    'yoruba': 'Yoruba',
+    'yoruba_literature': 'Yoruba Literature',
+    'management_in_living': 'Management in Living',
+    'mathematics': 'Mathematics',
+    'metalwork': 'Metalwork',
+    'music': 'Music',
+    'nigerian_language': 'Nigerian Language',
+    'office_practice': 'Office Practice',
+    'physical_education': 'Physical Education',
+    'physical_health_education': 'Physical and Health Education',
+    'physics': 'Physics',
+    'physics_alt_syl': 'Physics (Alt.Syl)',
+    'principles_and_practice_of_education': 'Principles and Practice of Education',
+    'principles_of_cost_accounting': 'Principles of Cost Accounting',
+    'rural_science': 'Rural Science',
+    'science': 'Science',
+    'secretarial_duties': 'Secretarial Duties',
+    'shorthand': 'Shorthand',
+    'sierra_leone_studies': 'Sierra Leone Studies',
+    'sierra_leone_languages': 'Sierra Leone Languages',
+    'social_studies': 'Social Studies',
+    'statistics': 'Statistics',
+    'teaching_practice': 'Teaching Practice',
+    'technical_drawing': 'Technical Drawing',
+    'typewriting': 'Typewriting',
+    'visual_art': 'Visual Art',
+    'woodwork': 'Woodwork',
+    'winding_elect_machines': 'Winding of Elect Machines and Elect Eng Sc',
+    'first_written': 'First Written',
+    'second_written': 'Second Written',
+    'pract_tests': 'Practical Tests',
+    # Added 25/08/2014
+    'animal_husbandry_(alt_a)': 'Animal Husbandry (Alt A)',
+    'animal_husbandry_(alt_b)': 'Animal Husbandry (Alt B)',
+    'auto_electrical_work': 'Auto Electrical Work',
+    'auto_mechanical_work': 'Auto Mechanical Work',
+    'catering_craft_practice': 'Catering Craft Practice',
+    'civic_education': 'Civic Education',
+    'computer_studies': 'Computer Studies',
+    'cosmetology': 'Cosmetology',
+    'data_processing': 'Data Processing',
+    'dyeing_&_bleaching': 'Dyeing & Bleaching',
+    'electrical_installation_and_maintenance_work': 'Electrical Installation and Maintenance Work',
+    'fisheries_(alt_a)_ghana_candidates': 'Fisheries (Alt A) Ghana Candidates',
+    'fisheries_(alt_b)_nigeria_candidates': 'Fisheries (Alt B) Nigeria Candidates',
+    'garment_making': 'Garment Making',
+    'general_agriculture': 'General Agriculture',
+    'gsm_phones_maintenance_and_repairs': 'Gsm Phones Maintenance and Repairs',
+    'health_education': 'Health Education',
+    'insurance': 'Insurance',
+    'painting_and_decorating': 'Painting and Decorating',
+    'photography': 'Photography',
+    'printing_craft_practice': 'Printing Craft Practice',
+    'store_management': 'Store Management',
+    'textiles': 'Textiles',
+    # Added 19/01/2015
+    'marketing': 'Marketing',
+    'carpentry_joinery': 'Carpentry and Joinery',
+    'tourism': 'Tourism',
+    'mining': 'Mining',
+    }
+
+EXAM_GRADES = (
+    ('A1', 'Excellent (A1)'),
+    ('A2', 'Very Good (A2)'),
+    ('A3', 'Good (A3)'),
+    ('B2', 'Very Good (B2)'),
+    ('B3', 'Good (B3)'),
+    ('C4', 'Credit (C4)'),
+    ('C5', 'Credit (C5)'),
+    ('C6', 'Credit (C6)'),
+    ('A', 'Pass (A)'),
+    ('B', 'Pass (B)'),
+    ('C', 'Pass (C)'),
+    ('U', 'Pass (U)'),
+    ('D7', 'Pass (D7)'),
+    ('P7', 'Pass (P7)'),
+    ('P8', 'Pass (P8)'),
+    ('E8', 'Pass (E8)'),
+    ('F9', 'Fail (F9)'),
+    ('Aa', 'A (a)'),
+    ('Bb', 'B (b)'),
+    ('Cc', 'C (c)'),
+    ('Dd', 'D (d)'),
+    ('Ee', 'E (e)'),
+    ('Ff', 'F (f)'),
+    ('Gg', 'G (g)'),
+    ('credit-1', 'Credit-1'),
+    ('credit-2', 'Credit-2'),
+    ('dist', 'Distinction'),
+    ('pass', 'Pass'),
+    ('AR', 'Awaiting Results'),
+    )
+
+
+class SubjectSource(BasicSourceFactory):
+    """A source for school subjects used in exam documentation.
+    """
+    def getValues(self):
+        return sorted(EXAM_SUBJECTS_DICT.keys())
+
+    def getTitle(self, value):
+        return "%s:" % EXAM_SUBJECTS_DICT[value]
+
+
+class GradeSource(BasicSourceFactory):
+    """A source for exam grades.
+    """
+    def getValues(self):
+        for entry in EXAM_GRADES:
+            yield entry[0]
+
+    def getTitle(self, value):
+        return dict(EXAM_GRADES)[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.
+    """
+
+
+#: A unique default value.
+DEFAULT_VALUE = object()
+
+
+class ResultEntry(grok.Model):
+    """A result entry contains a subject and a grade.
+    """
+    grok.implements(IResultEntry)
+    subject = FieldProperty(IResultEntry['subject'])
+    grade = FieldProperty(IResultEntry['grade'])
+
+    def __init__(self, subject=None, grade=None):
+        super(ResultEntry, self).__init__()
+        if subject is not None:
+            self.subject = subject
+        if grade is not None:
+            self.grade = grade
+        return
+
+    def __eq__(self, obj):
+        """Two ResultEntry objects are equal if their `subject` and
+           `grade` are equal.
+        """
+        for name in ('subject', 'grade',):
+            if getattr(self, name) != getattr(obj, name, DEFAULT_VALUE):
+                return False
+        return True
+
+    def __ne__(self, other):
+        """Two ResultEntries are not equal, if their equality test fails.
+
+        a != b <-> not(a == b). Python doc tell, that __ne__ should
+        also be rovided, whenever __eq__ is implemented.
+        """
+        return not self.__eq__(other)
+
+    def to_string(self):
+        """A string representation that can be used in exports.
+
+        Returned is a unicode string of format ``(u'<SUBJ>',u'<GRADE>')``.
+        """
+        return unicode((self.subject, self.grade))
+
+    @classmethod
+    def from_string(cls, string):
+        """Create new ResultEntry instance based on `string`.
+
+        The string is expected to be in format as delivered by
+        meth:`to_string`.
+
+        This is a classmethod. This means, you normally will call::
+
+          ResultEntry.from_string(mystring)
+
+        i.e. use the `ResultEntry` class, not an instance thereof.
+        """
+        string = string.replace("u''", "None")
+        subject, grade = eval(string)
+        return cls(subject, grade)
+
+
+class ResultEntryField(schema.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 IkobaObjectWidgets as default widgets for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def result_entry_input_widget(obj, req):
+    return IkobaObjectWidget(obj, req, ResultEntry)
+
+
+# register a display widget for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def result_entry_display_widget(obj, req):
+    return IkobaObjectDisplayWidget(obj, req, ResultEntry)
Index: /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/tests/test_schoolgrades.py
===================================================================
--- /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/tests/test_schoolgrades.py	(revision 12570)
+++ /main/ikobacustom.pcn/trunk/src/ikobacustom/pcn/customers/tests/test_schoolgrades.py	(revision 12570)
@@ -0,0 +1,105 @@
+# Tests for schoolgrades module.
+import unittest
+from zope.component import getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import ConstraintNotSatisfied
+from ikobacustom.pcn.customers.schoolgrades import (
+    IResultEntry, IResultEntryField,
+    ResultEntry, ResultEntryField,
+    EXAM_SUBJECTS_DICT, EXAM_GRADES)
+
+
+class ResultEntryTests(unittest.TestCase):
+
+    def setUp(self):
+        self.valid_subj = EXAM_SUBJECTS_DICT.keys()[0]
+        self.valid_grade = EXAM_GRADES[0][0]
+        return
+
+    def tearDown(self):
+        return
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntry()
+        verifyObject(IResultEntry, obj)
+        verifyClass(IResultEntry, ResultEntry)
+        return
+
+    def test_init(self):
+        # we can pass initial values
+        item1 = ResultEntry()
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        self.assertTrue(item1.subject is None)
+        self.assertTrue(item1.grade is None)
+        self.assertEqual(item2.subject, self.valid_subj)
+        self.assertEqual(item2.grade, self.valid_grade)
+        return
+
+    def test_illegal_value(self):
+        item = ResultEntry()
+        self.assertRaises(
+            ConstraintNotSatisfied, ResultEntry, 'invalid', 'invalid')
+        self.assertRaises(
+            ConstraintNotSatisfied, ResultEntry, 'invalid')
+        self.assertRaises(
+            ConstraintNotSatisfied, setattr, item, 'subject', 'blah')
+        self.assertRaises(
+            ConstraintNotSatisfied, setattr, item, 'grade', 'blah')
+        return
+
+    def test_to_string(self):
+        # the string representation is handy for export
+        item1 = ResultEntry()
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        self.assertEqual(item1.to_string(), u"(None, None)")
+        self.assertEqual(item2.to_string(), u"('%s', '%s')" % (
+            self.valid_subj, self.valid_grade))
+        return
+
+    def test_from_string(self):
+        # we can create new result entries based on strings
+        myinput = u"(u'%s',u'%s')" % (
+            self.valid_subj, self.valid_grade)
+        item1 = ResultEntry.from_string(myinput)
+        item2 = ResultEntry.from_string(u"(u'',u'')")
+        item3 = ResultEntry.from_string(u"(None, None)")
+        self.assertEqual(item1.subject, self.valid_subj)
+        self.assertEqual(item1.grade, self.valid_grade)
+        self.assertTrue(item2.subject is None)
+        self.assertTrue(item2.grade is None)
+        self.assertTrue(item3.subject is None)
+        self.assertTrue(item3.grade is None)
+        return
+
+    def test_eq(self):
+        # we can compare equality of ResultEntry objects
+        item1 = ResultEntry(self.valid_subj, self.valid_grade)
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        item3 = ResultEntry()
+        item4 = ResultEntry()
+        assert item1 is not item2
+        assert item1 == item1
+        assert item1 == item2
+        assert item3 is not item4
+        assert item3 == item4
+        assert item1.__eq__(item2) is True
+        assert item1.__eq__(item3) is False
+
+    def test_ne(self):
+        # we can also tell, which ResultEntries are _not_ equal
+        item1 = ResultEntry(self.valid_subj, self.valid_grade)
+        item2 = ResultEntry()
+        assert item1 != item2
+        assert item1.__ne__(item2) is True
+        assert item1.__ne__(item1) is False
+
+
+class ResultEntryFieldTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntryField()
+        verifyObject(IResultEntryField, obj)
+        verifyClass(IResultEntryField, ResultEntryField)
+        return
