Testing

Introduction

Kofa’s Python code is being tested automatically. The developers’ goal is to reach 100% code coverage by Kofa’s test runners, which means that each single line of code is passed at least one time when running the tests.

Why testing? Testing makes sure that our code works properly under given sets of conditions and, only comprehensive testing ensures that changing or customizing the code does not break existing functionality.

Why automated testing? Simply because no developer likes to click around the user interface to check tons of functionality. In Kofa more than 1300 tests, with dozens of actions per test, are being executed each time when the testrunner is started. This job can’t be done manually.

What we test: “Unit” and “Functional” Tests

There are two different ways to test the code of a web application automatically: unit tests and functional tests. The goal of unit testing is to isolate each part of the program and show that the individual parts are correct. Functional tests of a web application are more wholistic, they require a running web application in the background with even a working (temporary) database. Functional tests are typically linked to a particular use case, they are interacting with the portal as a user would. That implies that functional testing has to make use of a test browser. A test browser does the same job as normal web browser does, except for visualizing the rendered HTML code.

How we test: “Python” and “Doctest” Tests

There are also two different ways to integrate tests, either functional or unit, into a Python package: Doctest tests (or doctests) and Python tests. Python test modules are a collection of isolatable Python test cases. A test case combines a collection of test methods which are being executed by the testrunner one after the other. Python test modules are automatically identified by means of their filenames which start with test_. In contrast, doctests can be wrapped up in simple text files (ending with .txt), or they can be put into docstrings of the application’s source code itself. Common to all doctests is that they are based on output from the standard Python interpreter shell (indicated by the interpreter prompt >>>. The doctest runner parses all py and txt files in our package, executes the interpreter commands found and compares the output against an expected value. Example:

Python's `math` module can compute the square root of 2:

>>> from math import sqrt
>>> sqrt(2)
1.4142135623730951

Why wrapping tests into documentation? An objective of testdriven software development is also the documentation of the ‘Application Programming Interface’ (API) and, to a certain extent, providing a guideline to users, how to operate the portal. The first is mainly done in the docstrings of Python modules which present an expressive documentation of the main use cases of a module and its components. The latter is primarily done in separate txt files.

When starting the development of Kofa, we relied on writing a coherent documentation including doctests in restructured text format. During the software development process, the focus shifted from doctesting to pure Python testing with a lot of functional tests inside. It turned out that Python tests are easier to handle and more powerful. Drawback is, that these tests cannot easily be integrated into the Sphinx documentation project (the documentation which you are reading right now). However, we will list some of these tests and try to explain what they are good for.

Python Tests

There are hundreds of Python test cases in Kofa with many test methods each. Here we present only a few of them. The test methods are easy to read. In most cases they are functional and certain methods and properties of a test browser are called. Most important are browser.open() (opens a web page), browser.getControl() (gets a control button), browser.getLink() (gets a link) and browser.contents (is the HTML content of the opened page).

Suspended Officer Account

The first test can be found in waeup.kofa.browser.tests.test_browser.SupplementaryBrowserTests. The test makes sure that suspended officers can’t login but see a proper warning message when trying to login. Furthermore, suspended officer accounts are clearly marked and a warning message shows up if a manager accesses a suspended account, see Officers.

    def test_suspended_officer(self):
        self.app['users'].addUser(
            'officer', 'secret', title='Bob Officer', email='bob@abcd.ng')
        # Officer can't login if their password is not set.
        self.app['users']['officer'].password = None
        self.browser.open('http://localhost/app/login')
        self.browser.getControl(name="form.login").value = 'officer'
        self.browser.getControl(name="form.password").value = 'secret'
        self.browser.getControl("Login").click()
        self.assertTrue(
            'You entered invalid credentials.' in self.browser.contents)
        # We set the password again.
        IUserAccount(
            self.app['users']['officer']).setPassword('secret')
        # Officers can't login if their account is suspended/deactivated.
        self.app['users']['officer'].suspended = True
        self.browser.open('http://localhost/app/login')
        self.browser.getControl(name="form.login").value = 'officer'
        self.browser.getControl(name="form.password").value = 'secret'
        self.browser.getControl("Login").click()
        self.assertMatches(
            '...but yor account has been temporarily deactivated...',
            self.browser.contents)
        # Officer is really not logged in.
        self.assertFalse("Bob Officer" in self.browser.contents)
        self.app['users']['officer'].suspended = False
        self.browser.open('http://localhost/app/login')
        self.browser.getControl(name="form.login").value = 'officer'
        self.browser.getControl(name="form.password").value = 'secret'
        self.browser.getControl("Login").click()
        # Yeah, officer logged in.
        self.assertMatches(
            '...You logged in...', self.browser.contents)
        self.assertTrue("Bob Officer" in self.browser.contents)
        self.browser.getLink("Logout").click()
        # Login as manager.
        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
        # Suspended accounts are marked in red.
        self.browser.open('http://localhost/app/users')
        self.assertFalse('(suspended)' in self.browser.contents)
        self.app['users']['officer'].suspended = True
        self.browser.open('http://localhost/app/users')
        self.assertTrue(
            '<span style="color:red">(suspended)</span>'
            in self.browser.contents)
        # A flash message appears on the edit page of the officer.
        self.browser.open('http://localhost/app/users/officer')
        self.assertTrue(
            'This account has been suspended.' in self.browser.contents)
        self.app['users']['officer'].suspended = False
        self.browser.open('http://localhost/app/users/officer')
        self.assertFalse(
            'This account has been suspended.' in self.browser.contents)
        return

Handling Clearance by Clearance Officer

This test can be found in waeup.kofa.students.tests.test_browser.OfficerUITests. The corresponding use case is partly described elsewhere.

    def test_handle_clearance_by_co(self):
        self.init_clearance_officer()
        self.assertMatches('...You logged in...', self.browser.contents)
        # CO is landing on index page.
        self.assertEqual(self.browser.url, 'http://localhost/app/index')
        # CO can see his roles
        self.browser.getLink("My Roles").click()
        self.assertMatches(
            '...<div>Academics Officer (view only)</div>...',
            self.browser.contents)
        # But not his local role ...
        self.assertFalse('Clearance Officer' in self.browser.contents)
        # ... because we forgot to notify the department that the local role
        # has changed.
        notify(LocalRoleSetEvent(
            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
            granted=True))
        self.browser.open('http://localhost/app/users/mrclear/my_roles')
        self.assertTrue('Clearance Officer' in self.browser.contents)
        self.assertMatches(
            '...<a href="http://localhost/app/faculties/fac1/dep1">...',
            self.browser.contents)
        # CO can view the student ...
        self.browser.open(self.clearance_path)
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.url, self.clearance_path)
        # ... but not other students.
        self.assertRaises(
            Unauthorized, self.browser.open, self.other_student_path)
        # Clearance is disabled for this session.
        self.browser.open(self.clearance_path)
        self.assertFalse('Clear student' in self.browser.contents)
        self.browser.open(self.student_path + '/clear')
        self.assertTrue('Clearance is disabled for this session'
            in self.browser.contents)
        self.app['configuration']['2004'].clearance_enabled = True
        # Only in state clearance requested the CO does see the 'Clear' button.
        self.browser.open(self.clearance_path)
        self.assertFalse('Clear student' in self.browser.contents)
        self.browser.open(self.student_path + '/clear')
        self.assertTrue('Student is in wrong state.'
            in self.browser.contents)
        IWorkflowInfo(self.student).fireTransition('request_clearance')
        self.browser.open(self.clearance_path)
        self.assertTrue('Clear student' in self.browser.contents)
        self.browser.getLink("Clear student").click()
        self.assertTrue('Student has been cleared' in self.browser.contents)
        self.assertTrue('cleared' in self.browser.contents)
        self.browser.open(self.history_path)
        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
        # Hide real name.
        self.app['users']['mrclear'].public_name = 'My Public Name'
        self.browser.open(self.clearance_path)
        self.browser.getLink("Reject clearance").click()
        self.assertEqual(
            self.browser.url, self.student_path + '/reject_clearance')
        # Type comment why.
        self.browser.getControl(name="form.officer_comment").value = (
            'Dear Student,\n'
            'You did not fill properly.')
        self.browser.getControl("Save comment").click()
        self.assertTrue('Clearance has been annulled' in self.browser.contents)
        url = ('http://localhost/app/students/K1000000/'
              'contactstudent?body=Dear+Student%2C%0AYou+did+not+fill+properly.'
              '&subject=Clearance+has+been+annulled.')
        # CO does now see the prefilled contact form and can send a message.
        self.assertEqual(self.browser.url, url)
        self.assertTrue('clearance started' in self.browser.contents)
        self.assertTrue('name="form.subject" size="20" type="text" '
            'value="Clearance has been annulled."'
            in self.browser.contents)
        self.assertTrue('name="form.body" rows="10" >Dear Student,'
            in self.browser.contents)
        self.browser.getControl("Send message now").click()
        self.assertTrue('Your message has been sent' in self.browser.contents)
        # The comment has been stored ...
        self.assertEqual(self.student.officer_comment,
            u'Dear Student,\nYou did not fill properly.')
        # ... and logged.
        logfile = os.path.join(
            self.app['datacenter'].storage, 'logs', 'students.log')
        logcontent = open(logfile).read()
        self.assertTrue(
            'INFO - mrclear - students.browser.StudentRejectClearancePage - '
            'K1000000 - comment: Dear Student,<br>You did not fill '
            'properly.\n' in logcontent)
        self.browser.open(self.history_path)
        self.assertTrue("Reset to 'clearance started' by My Public Name" in
            self.browser.contents)
        IWorkflowInfo(self.student).fireTransition('request_clearance')
        self.browser.open(self.clearance_path)
        self.browser.getLink("Reject clearance").click()
        self.browser.getControl("Save comment").click()
        self.assertTrue('Clearance request has been rejected'
            in self.browser.contents)
        self.assertTrue('clearance started' in self.browser.contents)
        # The CO can't clear students if not in state
        # clearance requested.
        self.browser.open(self.student_path + '/clear')
        self.assertTrue('Student is in wrong state'
            in self.browser.contents)
        # The CO can go to his department throug the my_roles page ...
        self.browser.open('http://localhost/app/users/mrclear/my_roles')
        self.browser.getLink("http://localhost/app/faculties/fac1/dep1").click()
        # ... and view the list of students.
        self.browser.getLink("Show students").click()
        self.browser.getControl(name="session").value = ['2004']
        self.browser.getControl(name="level").value = ['200']
        self.browser.getControl("Show").click()
        self.assertFalse(self.student_id in self.browser.contents)
        self.browser.getControl(name="session").value = ['2004']
        self.browser.getControl(name="level").value = ['100']
        self.browser.getControl("Show").click()
        self.assertTrue(self.student_id in self.browser.contents)
        # The comment is indicated by 'yes'.
        self.assertTrue('<td><span>yes</span></td>' in self.browser.contents)
        # Check if the enquiries form is not pre-filled with officer_comment
        # (regression test).
        self.browser.getLink("Logout").click()
        self.browser.open('http://localhost/app/enquiries')
        self.assertFalse(
            'You did not fill properly'
            in self.browser.contents)
        # When a student is cleared the comment is automatically deleted
        IWorkflowInfo(self.student).fireTransition('request_clearance')
        IWorkflowInfo(self.student).fireTransition('clear')
        self.assertEqual(self.student.officer_comment, None)
        return

Handling Course List Validation by Course Adviser

This test can be found in waeup.kofa.students.tests.test_browser.OfficerUITests. The corresponding use case is described elsewhere.

    def test_handle_courses_by_ca(self):
        self.app['users'].addUser('mrsadvise', 'mrsadvisesecret')
        self.app['users']['mrsadvise'].email = 'mradvise@foo.ng'
        self.app['users']['mrsadvise'].title = u'Helen Procter'
        # Assign local CourseAdviser100 role for a certificate
        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
        prmlocal = IPrincipalRoleManager(cert)
        prmlocal.assignRoleToPrincipal('waeup.local.CourseAdviser100', 'mrsadvise')
        IWorkflowState(self.student).setState('school fee paid')
        # Login as course adviser.
        self.browser.open(self.login_path)
        self.browser.getControl(name="form.login").value = 'mrsadvise'
        self.browser.getControl(name="form.password").value = 'mrsadvisesecret'
        self.browser.getControl("Login").click()
        self.assertMatches('...You logged in...', self.browser.contents)
        # CO can see his roles.
        self.browser.getLink("My Roles").click()
        self.assertMatches(
            '...<div>Academics Officer (view only)</div>...',
            self.browser.contents)
        # But not his local role ...
        self.assertFalse('Course Adviser' in self.browser.contents)
        # ... because we forgot to notify the certificate that the local role
        # has changed.
        notify(LocalRoleSetEvent(
            cert, 'waeup.local.CourseAdviser100', 'mrsadvise', granted=True))
        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
        self.assertTrue('Course Adviser 100L' in self.browser.contents)
        self.assertMatches(
            '...<a href="http://localhost/app/faculties/fac1/dep1/certificates/CERT1">...',
            self.browser.contents)
        # CA can view the student ...
        self.browser.open(self.student_path)
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.url, self.student_path)
        # ... but not other students.
        other_student = Student()
        other_student.firstname = u'Dep2'
        other_student.lastname = u'Student'
        self.app['students'].addStudent(other_student)
        other_student_path = (
            'http://localhost/app/students/%s' % other_student.student_id)
        self.assertRaises(
            Unauthorized, self.browser.open, other_student_path)
        # We add study level 110 to the student's studycourse.
        studylevel = StudentStudyLevel()
        studylevel.level = 110
        self.student['studycourse'].addStudentStudyLevel(
            cert,studylevel)
        L110_student_path = self.studycourse_path + '/110'
        # The CA can neither see the Validate nor the Edit button.
        self.browser.open(L110_student_path)
        self.assertFalse('Validate courses' in self.browser.contents)
        self.assertFalse('Edit' in self.browser.contents)
        IWorkflowInfo(self.student).fireTransition('register_courses')
        self.browser.open(L110_student_path)
        self.assertFalse('Validate courses' in self.browser.contents)
        self.assertFalse('Edit' in self.browser.contents)
        # Only in state courses registered and only if the current level
        # corresponds with the name of the study level object
        # the 100L CA does see the 'Validate' button but not
        # the edit button.
        self.student['studycourse'].current_level = 110
        self.browser.open(L110_student_path)
        self.assertFalse('Edit' in self.browser.contents)
        self.assertTrue('Validate courses' in self.browser.contents)
        # But a 100L CA does not see the button at other levels.
        studylevel2 = StudentStudyLevel()
        studylevel2.level = 200
        self.student['studycourse'].addStudentStudyLevel(
            cert,studylevel2)
        L200_student_path = self.studycourse_path + '/200'
        self.browser.open(L200_student_path)
        self.assertFalse('Edit' in self.browser.contents)
        self.assertFalse('Validate courses' in self.browser.contents)
        self.browser.open(L110_student_path)
        self.browser.getLink("Validate courses").click()
        self.assertTrue('Course list has been validated' in self.browser.contents)
        self.assertTrue('courses validated' in self.browser.contents)
        self.assertEqual(self.student['studycourse']['110'].validated_by,
            'Helen Procter')
        self.assertMatches(
            '<YYYY-MM-DD hh:mm:ss>',
            self.student['studycourse']['110'].validation_date.strftime(
                "%Y-%m-%d %H:%M:%S"))
        self.browser.getLink("Reject courses").click()
        self.assertTrue('Course list request has been annulled.'
            in self.browser.contents)
        urlmessage = 'Course+list+request+has+been+annulled.'
        self.assertEqual(self.browser.url, self.student_path +
            '/contactstudent?subject=%s' % urlmessage)
        self.assertTrue('school fee paid' in self.browser.contents)
        self.assertTrue(self.student['studycourse']['110'].validated_by is None)
        self.assertTrue(self.student['studycourse']['110'].validation_date is None)
        IWorkflowInfo(self.student).fireTransition('register_courses')
        self.browser.open(L110_student_path)
        self.browser.getLink("Reject courses").click()
        self.assertTrue('Course list has been unregistered'
            in self.browser.contents)
        self.assertTrue('school fee paid' in self.browser.contents)
        # CA does now see the contact form and can send a message.
        self.browser.getControl(name="form.subject").value = 'Important subject'
        self.browser.getControl(name="form.body").value = 'Course list rejected'
        self.browser.getControl("Send message now").click()
        self.assertTrue('Your message has been sent' in self.browser.contents)
        # The CA does now see the Edit button and can edit
        # current study level.
        self.browser.open(L110_student_path)
        self.browser.getLink("Edit").click()
        self.assertTrue('Edit course list of 100 (Year 1) on 1st probation'
            in self.browser.contents)
        # The CA can't validate courses if not in state
        # courses registered.
        self.browser.open(L110_student_path + '/validate_courses')
        self.assertTrue('Student is in the wrong state'
            in self.browser.contents)
        # The CA can go to his certificate through the my_roles page ...
        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
        self.browser.getLink(
            "http://localhost/app/faculties/fac1/dep1/certificates/CERT1").click()
        # ... and view the list of students.
        self.browser.getLink("Show students").click()
        self.browser.getControl(name="session").value = ['2004']
        self.browser.getControl(name="level").value = ['100']
        self.browser.getControl("Show").click()
        self.assertTrue(self.student_id in self.browser.contents)

Batch Editing Scores by Lecturers

These test can be found in waeup.kofa.students.tests.test_browser.LecturerUITests. The corresponding use cases are described elsewhere.

class LecturerUITests(StudentsFullSetup):
    # Tests for UI actions when acting as lecturer.

    def login_as_lecturer(self):
        self.app['users'].addUser('mrslecturer', 'mrslecturersecret')
        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
        # Add course ticket
        studylevel = createObject(u'waeup.StudentStudyLevel')
        studylevel.level = 100
        studylevel.level_session = 2004
        self.student['studycourse'].addStudentStudyLevel(
            self.certificate, studylevel)
        # Assign local Lecturer role for a certificate.
        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
        prmlocal = IPrincipalRoleManager(course)
        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
        notify(LocalRoleSetEvent(
            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
        # Login as lecturer.
        self.browser.open(self.login_path)
        self.browser.getControl(name="form.login").value = 'mrslecturer'
        self.browser.getControl(
            name="form.password").value = 'mrslecturersecret'
        self.browser.getControl("Login").click()
        # Store reused urls/paths
        self.course_url = (
            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1')
        self.edit_scores_url = '%s/edit_scores' % self.course_url
        # Set standard parameters
        self.app['configuration'].current_academic_session = 2004
        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
        IWorkflowState(self.student).setState(VALIDATED)

    @property
    def stud_log_path(self):
        return os.path.join(
            self.app['datacenter'].storage, 'logs', 'students.log')

    def test_lecturer_lands_on_landing_page(self):
        # lecturers can login and will be led to landing page.
        self.login_as_lecturer()
        self.assertMatches('...You logged in...', self.browser.contents)
        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)

    def test_my_roles_link_works(self):
        # lecturers can see their roles
        self.login_as_lecturer()
        self.browser.getLink("My Roles").click()
        self.assertTrue(
            "<div>Academics Officer (view only)</div>"
            in self.browser.contents)
        self.assertTrue(
            '<a href="%s">' % self.course_url in self.browser.contents)

    def test_my_roles_page_contains_backlink(self):
        # we can get back from 'My Roles' view to landing page
        self.login_as_lecturer()
        self.browser.getLink("My Roles").click()
        self.browser.getLink("My Courses").click()
        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)

    def test_lecturers_can_reach_their_courses(self):
        # lecturers get links to their courses on the landing page
        self.login_as_lecturer()
        self.browser.getLink("COURSE1").click()
        self.assertEqual(self.browser.url, self.course_url)

    def test_lecturers_student_access_is_restricted(self):
        # lecturers are not able to change other student data
        self.login_as_lecturer()
        # Lecturers can neither filter students ...
        self.assertRaises(
            Unauthorized, self.browser.open, '%s/students' % self.course_url)
        # ... nor access the student ...
        self.assertRaises(
            Unauthorized, self.browser.open, self.student_path)
        # ... nor the respective course ticket since editing course
        # tickets by lecturers is not feasible.
        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
        self.assertRaises(
            Unauthorized, self.browser.open, course_ticket_path)

    def test_score_editing_requires_department_permit(self):
        # we get a warning if we try to update score while we are not allowed
        self.login_as_lecturer()
        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
        self.browser.open(self.course_url)
        self.browser.getLink("Update session 2004/2005 scores").click()
        self.assertTrue('Score editing disabled' in self.browser.contents)
        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
        self.browser.open(self.course_url)
        self.browser.getLink("Update session 2004/2005 scores").click()
        self.assertFalse('Score editing disabled' in self.browser.contents)

    def test_score_editing_requires_validated_students(self):
        # we can edit only scores of students whose courses have been
        # validated.
        self.login_as_lecturer()
        # set invalid student state
        IWorkflowState(self.student).setState(CREATED)
        self.browser.open(self.edit_scores_url)
        self.assertRaises(
            LookupError, self.browser.getControl, name="scores")
        # set valid student state
        IWorkflowState(self.student).setState(VALIDATED)
        self.browser.open(self.edit_scores_url)
        self.assertTrue(
            self.browser.getControl(name="scores:list") is not None)

    def test_score_editing_offers_only_current_scores(self):
        # only scores from current academic session can be edited
        self.login_as_lecturer()
        IWorkflowState(self.student).setState('courses validated')
        # with no academic session set
        self.app['configuration'].current_academic_session = None
        self.browser.open(self.edit_scores_url)
        self.assertRaises(
            LookupError, self.browser.getControl, name="scores")
        # with wrong academic session set
        self.app['configuration'].current_academic_session = 1999
        self.browser.open(self.edit_scores_url)
        self.assertRaises(
            LookupError, self.browser.getControl, name="scores")
        # with right academic session set
        self.app['configuration'].current_academic_session = 2004
        self.browser.reload()
        self.assertTrue(
            self.browser.getControl(name="scores:list") is not None)

    def test_score_editing_can_change_scores(self):
        # we can really change scores via edit_scores view
        self.login_as_lecturer()
        self.assertEqual(
            self.student['studycourse']['100']['COURSE1'].score, None)
        self.browser.open(self.edit_scores_url)
        self.browser.getControl(name="scores:list", index=0).value = '55'
        self.browser.getControl("Update scores").click()
        # the new value is stored in data
        self.assertEqual(
            self.student['studycourse']['100']['COURSE1'].score, 55)
        # the new value is displayed on page/prefilled in form
        self.assertEqual(
            self.browser.getControl(name="scores:list", index=0).value, '55')
        # The change has been logged
        with open(self.stud_log_path, 'r') as fd:
            self.assertTrue(
                'mrslecturer - students.browser.EditScoresPage - '
                'K1000000 100/COURSE1 score updated (55)' in fd.read())

    def test_scores_editing_scores_must_be_integers(self):
        # Non-integer scores won't be accepted.
        self.login_as_lecturer()
        self.browser.open(self.edit_scores_url)
        self.browser.getControl(name="scores:list", index=0).value = 'abc'
        self.browser.getControl("Update scores").click()
        self.assertTrue(
            'Error: Score(s) of following students have not been updated '
            '(only integers are allowed): Anna Tester.'
            in self.browser.contents)

    def test_scores_editing_allows_score_removal(self):
        # we can remove scores, once they were set
        self.login_as_lecturer()
        # without a prior value, we cannot remove
        self.student['studycourse']['100']['COURSE1'].score = None
        self.browser.open(self.edit_scores_url)
        self.browser.getControl(name="scores:list", index=0).value = ''
        self.browser.getControl("Update scores").click()
        logcontent = open(self.stud_log_path, 'r').read()
        self.assertFalse('COURSE1 score updated (None)' in logcontent)
        # now retry with some value set
        self.student['studycourse']['100']['COURSE1'].score = 55
        self.browser.getControl(name="scores:list", index=0).value = ''
        self.browser.getControl("Update scores").click()
        logcontent = open(self.stud_log_path, 'r').read()
        self.assertTrue('COURSE1 score updated (None)' in logcontent)

    def test_lecturers_can_download_course_tickets(self):
        # A course ticket slip can be downloaded
        self.login_as_lecturer()
        pdf_url = '%s/coursetickets.pdf' % self.course_url
        self.browser.open(pdf_url)
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(
            self.browser.headers['Content-Type'], 'application/pdf')
        path = os.path.join(samples_dir(), 'coursetickets.pdf')
        open(path, 'wb').write(self.browser.contents)
        print "Sample PDF coursetickets.pdf written to %s" % path

    def test_lecturers_can_download_scores_as_csv(self):
        # Lecturers can download course scores as CSV.
        self.login_as_lecturer()
        self.browser.open(self.edit_scores_url)
        self.browser.getLink("Download csv file").click()
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.headers['Content-Type'],
                         'text/csv; charset=UTF-8')
        self.assertEqual(self.browser.contents, 'matric_number,student_id,'
            'display_fullname,level,code,level_session,score\r\n234,'
            'K1000000,Anna Tester,100,COURSE1,2004,\r\n')

    def test_scores_csv_upload_available(self):
        # lecturers can upload a CSV file to set values.
        self.login_as_lecturer()
        # set value to change from
        self.student['studycourse']['100']['COURSE1'].score = 55
        self.browser.open(self.edit_scores_url)
        upload_ctrl = self.browser.getControl(name='uploadfile:file')
        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % '65')
        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
        self.browser.getControl("Update editable scores from").click()
        # value changed
        self.assertEqual(
            self.student['studycourse']['100']['COURSE1'].score, 65)

    def test_scores_csv_upload_ignored(self):
        # for many type of file contents we simply ignore uploaded data
        self.login_as_lecturer()
        self.student['studycourse']['100']['COURSE1'].score = 55
        self.browser.open(self.edit_scores_url)
        for content, mimetype, name in (
                # empty file
                ('', 'text/foo', 'my.foo'),
                # plain ASCII text, w/o comma
                ('abcdef' * 200, 'text/plain', 'my.txt'),
                # plain UTF-8 text, with umlauts
                ('umlauts: äöü', 'text/plain', 'my.txt'),
                # csv file with only a header row
                ('student_id,score', 'text/csv', 'my.csv'),
                # csv with student_id column missing
                ('foo,score\r\nbar,66\r\n', 'text/csv', 'my.csv'),
                # csv with score column missing
                ('student_id,foo\r\nK1000000,bar\r\n', 'text/csv', 'my.csv'),
                # csv with non number as score value
                (UPLOAD_CSV_TEMPLATE % 'not-a-number', 'text/csv', 'my.csv'),
                ):
            upload_ctrl = self.browser.getControl(name='uploadfile:file')
            upload_ctrl.add_file(StringIO(content), mimetype, name)
            self.browser.getControl("Update scores").click()
            self.assertEqual(
                self.student['studycourse']['100']['COURSE1'].score, 55)
            self.assertFalse(
                'Uploaded file contains illegal data' in self.browser.contents)

    def test_scores_csv_upload_warn_illegal_chars(self):
        # for some types of files we issue a warning if upload data
        # contains illegal chars (and ignore the data)
        self.login_as_lecturer()
        self.student['studycourse']['100']['COURSE1'].score = 55
        self.browser.open(self.edit_scores_url)
        for content, mimetype, name in (
                # plain ASCII text, commas, control chars
                ('abv,qwe\n\r\r\t\b\n' * 20, 'text/plain', 'my.txt'),
                # image data (like a JPEG image)
                (open(SAMPLE_IMAGE, 'rb').read(), 'image/jpg', 'my.jpg'),
                ):
            upload_ctrl = self.browser.getControl(name='uploadfile:file')
            upload_ctrl.add_file(StringIO(content), mimetype, name)
            self.browser.getControl("Update editable scores").click()
            self.assertEqual(
                self.student['studycourse']['100']['COURSE1'].score, 55)
            self.assertTrue(
                'Uploaded file contains illegal data' in self.browser.contents)

Manage Hostel

This test can be found in waeup.kofa.hostels.tests.HostelsUITests. The corresponding use case is described elsewhere.

    def test_add_search_edit_delete_manage_hostels(self):
        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
        self.browser.open(self.container_path)
        self.browser.getLink("Manage accommodation").click()
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.url, self.manage_container_path)
        self.browser.getControl("Add hostel").click()
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.url, self.add_hostel_path)
        self.browser.getControl("Create hostel").click()
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertTrue('Hostel created' in self.browser.contents)
        self.browser.open(self.container_path + '/addhostel')
        self.browser.getControl("Create hostel").click()
        self.assertTrue('The hostel already exists' in self.browser.contents)
        hall = self.app['hostels']['hall-1']
        hall.blocks_for_female = ['A','B']
        self.browser.open(self.container_path + '/hall-1')
        expected = '...<ul id="form.blocks_for_female" ><li>Block A</li>...'
        self.assertMatches(expected,self.browser.contents)
        self.browser.open(self.container_path + '/hall-1/manage')
        self.browser.getControl(name="form.rooms_per_floor").value = '1'
        self.browser.getControl("Save").click()
        self.assertTrue('Form has been saved' in self.browser.contents)
        # Since the testbrowser does not support Javascrip the
        # save action cleared the settings above and we have to set them
        # again.
        self.assertTrue(len(hall.blocks_for_female) == 0)
        hall.blocks_for_female = ['A','B']
        hall.beds_for_fresh = ['A']
        hall.beds_for_returning = ['B']
        hall.beds_for_final = ['C']
        hall.beds_for_all = ['D','E']
        self.browser.getControl("Update all beds").click()
        self.assertTrue('Portal must be in maintenance mode for bed updates'
            in self.browser.contents)
        grok.getSite()['configuration'].maintmode_enabled_by = u'any_id'
        self.browser.getControl("Update all beds").click()
        expected = '...0 empty beds removed, 10 beds added, 0 occupied beds modified ()...'
        self.assertMatches(expected,self.browser.contents)
        cat = queryUtility(ICatalog, name='beds_catalog')
        results = cat.searchResults(
            bed_type=('regular_female_all', 'regular_female_all'))
        results = [(x.bed_id, x.bed_type) for x in results]
        self.assertEqual(results,
            [(u'hall-1_A_101_D', u'regular_female_all'),
             (u'hall-1_A_101_E', u'regular_female_all'),
             (u'hall-1_B_101_D', u'regular_female_all'),
             (u'hall-1_B_101_E', u'regular_female_all')])
        # Reserve beds.
        self.browser.getControl("Switch reservation", index=0).click()
        self.assertTrue('No item selected' in self.browser.contents)
        ctrl = self.browser.getControl(name='val_id')
        ctrl.getControl(value='hall-1_A_101_A').selected = True
        ctrl.getControl(value='hall-1_A_101_B').selected = True
        ctrl.getControl(value='hall-1_A_101_C').selected = True
        ctrl.getControl(value='hall-1_A_101_D').selected = True
        self.browser.getControl("Switch reservation", index=0).click()
        self.assertTrue('Successfully switched beds: hall-1_A_101_A (reserved)'
            in self.browser.contents)
        self.assertEqual(self.app['hostels']['hall-1'][
            'hall-1_A_101_D'].bed_type, 'regular_female_reserved')
        # The catalog has been updated.
        results = cat.searchResults(
            bed_type=('regular_female_all', 'regular_female_all'))
        results = [(x.bed_id, x.bed_type) for x in results]
        self.assertEqual(results,
            [(u'hall-1_A_101_E', u'regular_female_all'),
             (u'hall-1_B_101_D', u'regular_female_all'),
             (u'hall-1_B_101_E', u'regular_female_all')])
        results = cat.searchResults(
            bed_type=('regular_female_reserved', 'regular_female_reserved'))
        results = [(x.bed_id, x.bed_type) for x in results]
        self.assertEqual(results,
            [(u'hall-1_A_101_A', u'regular_female_reserved'),
             (u'hall-1_A_101_B', u'regular_female_reserved'),
             (u'hall-1_A_101_C', u'regular_female_reserved'),
             (u'hall-1_A_101_D', u'regular_female_reserved')])
        # Change hostel configuration with one bed booked.
        hall['hall-1_A_101_E'].owner = u'anyid'
        notify(grok.ObjectModifiedEvent(hall['hall-1_A_101_E']))
        hall.beds_for_fresh = ['A', 'E']
        hall.beds_for_all = ['D']
        self.browser.getControl("Update all beds").click()
        expected = '...9 empty beds removed, 9 beds added, 1 occupied beds modified...'
        self.assertMatches(expected,self.browser.contents)
        # Updating beds (including booked beds!) does update catalog.
        results = cat.searchResults(
            bed_type=('regular_female_all', 'regular_female_all'))
        results = [(x.bed_id, x.bed_type) for x in results]
        # The reservation of hall-1_A_101_D has been cancelled.
        self.assertEqual(results,
            [(u'hall-1_A_101_D', u'regular_female_all'),
             (u'hall-1_B_101_D', u'regular_female_all')])
        # Release bed which has previously been booked.
        bedticket = BedTicket()
        bedticket.booking_session = 2004
        bedticket.bed_coordinates = u'anything'
        self.student['accommodation'].addBedTicket(bedticket)
        self.app['hostels']['hall-1']['hall-1_A_101_D'].owner = self.student_id
        notify(grok.ObjectModifiedEvent(self.app['hostels']['hall-1']['hall-1_A_101_D']))
        self.browser.open(self.container_path + '/hall-1/manage')
        ctrl = self.browser.getControl(name='val_id')
        self.browser.getControl("Release selected beds", index=0).click()
        self.assertMatches("...No item selected...", self.browser.contents)
        ctrl = self.browser.getControl(name='val_id')
        ctrl.getControl(value='hall-1_A_101_D').selected = True
        self.browser.getControl("Release selected beds", index=0).click()
        self.assertMatches(
          '...Successfully released beds: hall-1_A_101_D (%s)...' % self.student_id,
          self.browser.contents)
        self.assertMatches(bedticket.bed_coordinates,
          u' -- booking cancelled on <YYYY-MM-DD hh:mm:ss> UTC --')
        # The catalog has been updated.
        results = cat.searchResults(owner=(self.student_id, self.student_id))
        assert len(results) == 0
        # If we release a free bed, nothing will happen.
        ctrl = self.browser.getControl(name='val_id')
        ctrl.getControl(value='hall-1_A_101_D').selected = True
        self.browser.getControl("Release selected beds", index=0).click()
        self.assertMatches(
          '...No allocated bed selected...', self.browser.contents)
        # Managers can manually allocate eligible students after cancellation.
        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_A')
        # 'not occupied' is not accepted.
        self.browser.getControl("Save").click()
        self.assertMatches(
            "...No valid student id...",
            self.browser.contents)
        # Invalid student ids are not accepted.
        self.browser.getControl(name="form.owner").value = 'nonsense'
        self.browser.getControl("Save").click()
        self.assertMatches(
            "...Either student does not exist or student "
            "is not in accommodation session...",
            self.browser.contents)
        self.browser.getControl(name="form.owner").value = self.student_id
        self.browser.getControl("Save").click()
        self.assertMatches("...Form has been saved...", self.browser.contents)
        # Students can only be allocated once.
        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_B')
        self.browser.getControl(name="form.owner").value = self.student_id
        self.browser.getControl("Save").click()
        self.assertMatches(
            "...This student resides in bed hall-1_A_101_A...",
            self.browser.contents)
        # If we open the same form again, we will be redirected to hostel
        # manage page. Beds must be released first before they can be
        # allocated to other students.
        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_A')
        self.assertEqual(self.browser.url,
            self.container_path + '/hall-1/@@manage#tab2')
        # Updating the beds again will not affect the allocation and also
        # the bed numbering remains the same.
        old_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
        old_owner = self.app['hostels']['hall-1']['hall-1_A_101_A'].owner
        self.browser.getControl("Update all beds").click()
        # 8 beds have been removed and re-added, 2 beds remains untouched
        # because they are occupied.
        expected = '...8 empty beds removed, 8 beds added, 0 occupied beds modified...'
        self.assertMatches(expected,self.browser.contents)
        new_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
        new_owner = self.app['hostels']['hall-1']['hall-1_A_101_A'].owner
        self.assertEqual(new_number, old_number)
        self.assertEqual(new_owner, old_owner)
        # If we change the bed type of an allocated bed, the modification will
        # be indicated.
        hall.blocks_for_female = ['B']
        hall.blocks_for_male = ['A']
        self.browser.getControl("Update all beds").click()
        expected = '...8 empty beds removed, 8 beds added, ' + \
            '2 occupied beds modified (hall-1_A_101_A, hall-1_A_101_E, )...'
        self.assertMatches(expected,self.browser.contents)
        new_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
        # Also the number of the bed has changed.
        self.assertFalse(new_number == old_number)
        # The number of occupied beds are displayed on container page.
        self.browser.open(self.container_path)
        self.assertTrue('2 of 10' in self.browser.contents)
        bedticket.bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
        # Managers can open the bed statistics page
        self.browser.getLink("Bed statistics").click()
        self.assertTrue('Bed Statistics</h1>' in self.browser.contents)
        # Remove entire hostel.
        self.browser.open(self.manage_container_path)
        ctrl = self.browser.getControl(name='val_id')
        value = ctrl.options[0]
        ctrl.getControl(value=value).selected = True
        self.browser.getControl("Remove selected", index=0).click()
        self.assertTrue('Successfully removed' in self.browser.contents)
        # Catalog is empty.
        results = cat.searchResults(
            bed_type=('regular_female_all', 'regular_female_all'))
        results = [x for x in results]
        assert len(results) == 0
        # Bed has been removed from bedticket
        self.assertEqual(bedticket.bed, None)
        # Actions are logged.
        logcontent = open(self.logfile).read()
        self.assertTrue(
            'hall-1 - 9 empty beds removed, 9 beds added, 1 occupied '
            'beds modified (hall-1_A_101_E, )'
            in logcontent)

Bed Space Booking

This test can be found in waeup.kofa.students.tests.test_browser.StudentUITests. The corresponding use case is described elsewhere.

    def test_student_accommodation(self):
        # Create a second hostel with one bed
        hostel = Hostel()
        hostel.hostel_id = u'hall-2'
        hostel.hostel_name = u'Hall 2'
        self.app['hostels'].addHostel(hostel)
        bed = Bed()
        bed.bed_id = u'hall-2_A_101_A'
        bed.bed_number = 1
        bed.owner = NOT_OCCUPIED
        bed.bed_type = u'regular_female_fr'
        self.app['hostels'][hostel.hostel_id].addBed(bed)

        self.browser.open(self.login_path)
        self.browser.getControl(name="form.login").value = self.student_id
        self.browser.getControl(name="form.password").value = 'spwd'
        self.browser.getControl("Login").click()
        # Students can add online booking fee payment tickets and open the
        # callback view (see test_manage_payments).
        self.browser.getLink("Payments").click()
        self.browser.getLink("Add current session payment ticket").click()
        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
        self.browser.getControl("Create ticket").click()
        ctrl = self.browser.getControl(name='val_id')
        value = ctrl.options[0]
        self.browser.getLink(value).click()
        self.browser.open(self.browser.url + '/fake_approve')
        # The new HOS-0 pin has been created.
        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
        pin = self.app['accesscodes']['HOS-0'].keys()[0]
        ac = self.app['accesscodes']['HOS-0'][pin]
        parts = pin.split('-')[1:]
        sfeseries, sfenumber = parts
        # Students can use HOS code and book a bed space with it ...
        self.browser.open(self.acco_path)
        # ... but not if booking period has expired ...
        self.app['hostels'].enddate = datetime.now(pytz.utc)
        self.browser.getControl("Book accommodation").click()
        self.assertMatches('...Outside booking period: ...',
                           self.browser.contents)
        self.app['hostels'].enddate = datetime.now(pytz.utc) + timedelta(days=10)
        # ... or student data are incomplete ...
        self.student['studycourse'].current_level = None
        self.browser.getControl("Book accommodation").click()
        self.assertMatches('...Your data are incomplete...',
            self.browser.contents)
        self.student['studycourse'].current_level = 100
        # ... or student is not the an allowed state ...
        self.browser.getControl("Book accommodation").click()
        self.assertMatches('...You are in the wrong...',
                           self.browser.contents)
        # Students can still not see the disired hostel selector.
        self.assertFalse('desired hostel' in self.browser.contents)
        IWorkflowInfo(self.student).fireTransition('admit')
        # Students can now see the disired hostel selector.
        self.browser.reload()
        self.browser.open(self.acco_path)
        self.assertTrue('desired hostel' in self.browser.contents)
        self.browser.getControl(name="hostel").value = ['hall-2']
        self.browser.getControl("Save").click()
        self.assertTrue('selection has been saved' in self.browser.contents)
        self.assertTrue('<option selected="selected" value="hall-2">'
            in self.browser.contents)
        self.browser.getControl("Book accommodation").click()
        self.assertMatches('...Activation Code:...',
                           self.browser.contents)
        # Student can't use faked ACs ...
        self.browser.getControl(name="ac_series").value = u'nonsense'
        self.browser.getControl(name="ac_number").value = sfenumber
        self.browser.getControl("Create bed ticket").click()
        self.assertMatches('...Activation code is invalid...',
                           self.browser.contents)
        # ... or ACs owned by somebody else.
        ac.owner = u'Anybody'
        self.browser.getControl(name="ac_series").value = sfeseries
        self.browser.getControl(name="ac_number").value = sfenumber
        self.browser.getControl("Create bed ticket").click()
        # Hostel 2 has only a bed for women.
        self.assertTrue('There is no free bed in your desired hostel'
            in self.browser.contents)
        self.browser.getControl(name="hostel").value = ['hall-1']
        self.browser.getControl("Save").click()
        self.browser.getControl("Book accommodation").click()
        # Student can't use faked ACs ...
        self.browser.getControl(name="ac_series").value = sfeseries
        self.browser.getControl(name="ac_number").value = sfenumber
        self.browser.getControl("Create bed ticket").click()
        self.assertMatches('...You are not the owner of this access code...',
                           self.browser.contents)
        # The bed remains empty.
        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
        self.assertTrue(bed.owner == NOT_OCCUPIED)
        ac.owner = self.student_id
        self.browser.getControl(name="ac_series").value = sfeseries
        self.browser.getControl(name="ac_number").value = sfenumber
        self.browser.getControl("Create bed ticket").click()
        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
                           self.browser.contents)
        # Bed has been allocated.
        self.assertTrue(bed.owner == self.student_id)
        # BedTicketAddPage is now blocked.
        self.browser.getControl("Book accommodation").click()
        self.assertMatches('...You already booked a bed space...',
            self.browser.contents)
        # The bed ticket displays the data correctly.
        self.browser.open(self.acco_path + '/2004')
        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
                           self.browser.contents)
        self.assertMatches('...2004/2005...', self.browser.contents)
        self.assertMatches('...regular_male_fr...', self.browser.contents)
        self.assertMatches('...%s...' % pin, self.browser.contents)
        # Students can open the pdf slip.
        self.browser.open(self.browser.url + '/bed_allocation_slip.pdf')
        self.assertEqual(self.browser.headers['Status'], '200 Ok')
        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
        # Students can't relocate themselves.
        self.assertFalse('Relocate' in self.browser.contents)
        relocate_path = self.acco_path + '/2004/relocate'
        self.assertRaises(
            Unauthorized, self.browser.open, relocate_path)
        # Students can't see the Remove button and check boxes.
        self.browser.open(self.acco_path)
        self.assertFalse('Remove' in self.browser.contents)
        self.assertFalse('val_id' in self.browser.contents)
        # Students can pay maintenance fee now.
        self.browser.open(self.payments_path)
        self.browser.open(self.payments_path + '/addop')
        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
        self.browser.getControl("Create ticket").click()
        self.assertMatches('...Payment ticket created...',
                           self.browser.contents)
        ctrl = self.browser.getControl(name='val_id')
        value = ctrl.options[0]
        # Maintennace fee is taken from the hostel object.
        self.assertEqual(self.student['payments'][value].amount_auth, 876.0)
        # If the hostel's maintenance fee isn't set, the fee is
        # taken from the session configuration object.
        self.app['hostels']['hall-1'].maint_fee = 0.0
        self.browser.open(self.payments_path + '/addop')
        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
        self.browser.getControl("Create ticket").click()
        ctrl = self.browser.getControl(name='val_id')
        value = ctrl.options[1]
        self.assertEqual(self.student['payments'][value].amount_auth, 987.0)
        # The bedticket is aware of successfull maintenance fee payment
        bedticket = self.student['accommodation']['2004']
        self.assertFalse(bedticket.maint_payment_made)
        self.student['payments'][value].approve()
        self.assertTrue(bedticket.maint_payment_made)
        return