source: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py @ 13757

Last change on this file since 13757 was 13711, checked in by Henrik Bettermann, 9 years ago

Add flash_notice field.

  • Property svn:keywords set to Id
File size: 202.1 KB
Line 
1## $Id: test_browser.py 13711 2016-02-18 11:52:18Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19Test the student-related UI components.
20"""
21import shutil
22import tempfile
23import pytz
24import base64
25from datetime import datetime, timedelta, date
26from StringIO import StringIO
27import os
28import grok
29from zc.async.testing import wait_for_result
30from zope.event import notify
31from zope.component import createObject, queryUtility, getUtility
32from zope.component.hooks import setSite, clearSite
33from zope.catalog.interfaces import ICatalog
34from zope.security.interfaces import Unauthorized
35from zope.securitypolicy.interfaces import IPrincipalRoleManager
36from zope.testbrowser.testing import Browser
37from zope.interface import implementedBy
38from zope.schema.fieldproperty import FieldProperty
39from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
40from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
41from waeup.kofa.app import University
42from waeup.kofa.payments.interfaces import IPayer
43from waeup.kofa.students.interfaces import IStudentsUtils
44from waeup.kofa.students.student import Student
45from waeup.kofa.students.studylevel import StudentStudyLevel
46from waeup.kofa.university.faculty import Faculty
47from waeup.kofa.university.department import Department
48from waeup.kofa.interfaces import IUserAccount, IJobManager
49from waeup.kofa.authentication import LocalRoleSetEvent
50from waeup.kofa.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
51from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
52from waeup.kofa.browser.tests.test_pdf import samples_dir
53
54PH_LEN = 15911  # Length of placeholder file
55
56SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
57SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
58
59def lookup_submit_value(name, value, browser):
60    """Find a button with a certain value."""
61    for num in range(0, 100):
62        try:
63            button = browser.getControl(name=name, index=num)
64            if button.value.endswith(value):
65                return button
66        except IndexError:
67            break
68    return None
69
70class StudentsFullSetup(FunctionalTestCase):
71    # A test case that only contains a setup and teardown
72    #
73    # Complete setup for students handlings is rather complex and
74    # requires lots of things created before we can start. This is a
75    # setup that does all this, creates a university, creates PINs,
76    # etc.  so that we do not have to bother with that in different
77    # test cases.
78
79    layer = FunctionalLayer
80
81    def setUp(self):
82        super(StudentsFullSetup, self).setUp()
83
84        # Setup a sample site for each test
85        app = University()
86        self.dc_root = tempfile.mkdtemp()
87        app['datacenter'].setStoragePath(self.dc_root)
88
89        # Prepopulate the ZODB...
90        self.getRootFolder()['app'] = app
91        # we add the site immediately after creation to the
92        # ZODB. Catalogs and other local utilities are not setup
93        # before that step.
94        self.app = self.getRootFolder()['app']
95        # Set site here. Some of the following setup code might need
96        # to access grok.getSite() and should get our new app then
97        setSite(app)
98
99        # Add student with subobjects
100        student = createObject('waeup.Student')
101        student.firstname = u'Anna'
102        student.lastname = u'Tester'
103        student.reg_number = u'123'
104        student.matric_number = u'234'
105        student.sex = u'm'
106        student.email = 'aa@aa.ng'
107        student.phone = u'1234'
108        student.date_of_birth = date(1981, 2, 4)
109        self.app['students'].addStudent(student)
110        self.student_id = student.student_id
111        self.student = self.app['students'][self.student_id]
112
113        # Set password
114        IUserAccount(
115            self.app['students'][self.student_id]).setPassword('spwd')
116
117        self.login_path = 'http://localhost/app/login'
118        self.container_path = 'http://localhost/app/students'
119        self.manage_container_path = self.container_path + '/@@manage'
120        self.add_student_path = self.container_path + '/addstudent'
121        self.student_path = self.container_path + '/' + self.student_id
122        self.manage_student_path = self.student_path + '/manage_base'
123        self.trigtrans_path = self.student_path + '/trigtrans'
124        self.clearance_path = self.student_path + '/view_clearance'
125        self.personal_path = self.student_path + '/view_personal'
126        self.edit_clearance_path = self.student_path + '/cedit'
127        self.manage_clearance_path = self.student_path + '/manage_clearance'
128        self.edit_personal_path = self.student_path + '/edit_personal'
129        self.manage_personal_path = self.student_path + '/manage_personal'
130        self.studycourse_path = self.student_path + '/studycourse'
131        self.payments_path = self.student_path + '/payments'
132        self.acco_path = self.student_path + '/accommodation'
133        self.history_path = self.student_path + '/history'
134
135        # Create 5 access codes with prefix'PWD'
136        pin_container = self.app['accesscodes']
137        pin_container.createBatch(
138            datetime.utcnow(), 'some_userid', 'PWD', 9.99, 5)
139        pins = pin_container['PWD-1'].values()
140        self.pwdpins = [x.representation for x in pins]
141        self.existing_pwdpin = self.pwdpins[0]
142        parts = self.existing_pwdpin.split('-')[1:]
143        self.existing_pwdseries, self.existing_pwdnumber = parts
144        # Create 5 access codes with prefix 'CLR'
145        pin_container.createBatch(
146            datetime.now(), 'some_userid', 'CLR', 9.99, 5)
147        pins = pin_container['CLR-1'].values()
148        pins[0].owner = u'Hans Wurst'
149        self.existing_clrac = pins[0]
150        self.existing_clrpin = pins[0].representation
151        parts = self.existing_clrpin.split('-')[1:]
152        self.existing_clrseries, self.existing_clrnumber = parts
153        # Create 2 access codes with prefix 'HOS'
154        pin_container.createBatch(
155            datetime.now(), 'some_userid', 'HOS', 9.99, 2)
156        pins = pin_container['HOS-1'].values()
157        self.existing_hosac = pins[0]
158        self.existing_hospin = pins[0].representation
159        parts = self.existing_hospin.split('-')[1:]
160        self.existing_hosseries, self.existing_hosnumber = parts
161
162        # Populate university
163        self.certificate = createObject('waeup.Certificate')
164        self.certificate.code = u'CERT1'
165        self.certificate.application_category = 'basic'
166        self.certificate.study_mode = 'ug_ft'
167        self.certificate.start_level = 100
168        self.certificate.end_level = 500
169        self.certificate.school_fee_1 = 40000.0
170        self.certificate.school_fee_2 = 20000.0
171        self.app['faculties']['fac1'] = Faculty(code=u'fac1')
172        self.app['faculties']['fac1']['dep1'] = Department(code=u'dep1')
173        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
174            self.certificate)
175        self.course = createObject('waeup.Course')
176        self.course.code = 'COURSE1'
177        self.course.semester = 1
178        self.course.credits = 10
179        self.course.passmark = 40
180        self.app['faculties']['fac1']['dep1'].courses.addCourse(
181            self.course)
182        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
183            self.course, level=100)
184
185        # Configure university and hostels
186        self.app['hostels'].accommodation_states = ['admitted']
187        self.app['hostels'].accommodation_session = 2004
188        delta = timedelta(days=10)
189        self.app['hostels'].startdate = datetime.now(pytz.utc) - delta
190        self.app['hostels'].enddate = datetime.now(pytz.utc) + delta
191        self.app['configuration'].carry_over = True
192        configuration = createObject('waeup.SessionConfiguration')
193        configuration.academic_session = 2004
194        configuration.clearance_fee = 3456.0
195        configuration.transcript_fee = 4567.0
196        configuration.booking_fee = 123.4
197        configuration.maint_fee = 987.0
198        configuration.transfer_fee = 456.0
199        configuration.late_registration_fee = 345.0
200        self.app['configuration'].addSessionConfiguration(configuration)
201
202        # Create a hostel with two beds
203        hostel = Hostel()
204        hostel.hostel_id = u'hall-1'
205        hostel.hostel_name = u'Hall 1'
206        hostel.maint_fee = 876.0
207        self.app['hostels'].addHostel(hostel)
208        bed = Bed()
209        bed.bed_id = u'hall-1_A_101_A'
210        bed.bed_number = 1
211        bed.owner = NOT_OCCUPIED
212        bed.bed_type = u'regular_male_fr'
213        self.app['hostels'][hostel.hostel_id].addBed(bed)
214        bed = Bed()
215        bed.bed_id = u'hall-1_A_101_B'
216        bed.bed_number = 2
217        bed.owner = NOT_OCCUPIED
218        bed.bed_type = u'regular_female_fr'
219        self.app['hostels'][hostel.hostel_id].addBed(bed)
220
221        # Set study course attributes of test student
222        self.student['studycourse'].certificate = self.certificate
223        self.student['studycourse'].current_session = 2004
224        self.student['studycourse'].entry_session = 2004
225        self.student['studycourse'].current_verdict = 'A'
226        self.student['studycourse'].current_level = 100
227        # Update the catalog
228        notify(grok.ObjectModifiedEvent(self.student))
229
230        # Put the prepopulated site into test ZODB and prepare test
231        # browser
232        self.browser = Browser()
233        self.browser.handleErrors = False
234
235    def tearDown(self):
236        super(StudentsFullSetup, self).tearDown()
237        clearSite()
238        shutil.rmtree(self.dc_root)
239
240
241
242class StudentsContainerUITests(StudentsFullSetup):
243    # Tests for StudentsContainer class views and pages
244
245    layer = FunctionalLayer
246
247    def test_anonymous_access(self):
248        # Anonymous users can't access students containers
249        self.assertRaises(
250            Unauthorized, self.browser.open, self.container_path)
251        self.assertRaises(
252            Unauthorized, self.browser.open, self.manage_container_path)
253        return
254
255    def test_manage_access(self):
256        # Managers can access the view page of students
257        # containers and can perform actions
258        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
259        self.browser.open(self.container_path)
260        self.assertEqual(self.browser.headers['Status'], '200 Ok')
261        self.assertEqual(self.browser.url, self.container_path)
262        self.browser.getLink("Manage students section").click()
263        self.assertEqual(self.browser.headers['Status'], '200 Ok')
264        self.assertEqual(self.browser.url, self.manage_container_path)
265        return
266
267    def test_add_search_delete_students(self):
268        # Managers can add search and remove students
269        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
270        self.browser.open(self.manage_container_path)
271        self.browser.getLink("Add student").click()
272        self.assertEqual(self.browser.headers['Status'], '200 Ok')
273        self.assertEqual(self.browser.url, self.add_student_path)
274        self.browser.getControl(name="form.firstname").value = 'Bob'
275        self.browser.getControl(name="form.lastname").value = 'Tester'
276        self.browser.getControl(name="form.reg_number").value = '123'
277        self.browser.getControl("Create student").click()
278        self.assertTrue('Registration number exists already'
279            in self.browser.contents)
280        self.browser.getControl(name="form.reg_number").value = '1234'
281        self.browser.getControl("Create student").click()
282        self.assertTrue('Student record created' in self.browser.contents)
283
284        # Registration and matric numbers must be unique
285        self.browser.getLink("Manage").click()
286        self.browser.getControl(name="form.reg_number").value = '123'
287        self.browser.getControl("Save").click()
288        self.assertMatches('...Registration number exists...',
289                           self.browser.contents)
290        self.browser.getControl(name="form.reg_number").value = '789'
291        self.browser.getControl(name="form.matric_number").value = '234'
292        self.browser.getControl("Save").click()
293        self.assertMatches('...Matriculation number exists...',
294                           self.browser.contents)
295
296        # We can find a student with a certain student_id
297        self.browser.open(self.container_path)
298        self.browser.getControl("Find student(s)").click()
299        self.assertTrue('Empty search string' in self.browser.contents)
300        self.browser.getControl(name="searchtype").value = ['student_id']
301        self.browser.getControl(name="searchterm").value = self.student_id
302        self.browser.getControl("Find student(s)").click()
303        self.assertTrue('Anna Tester' in self.browser.contents)
304
305        # We can find a student in a certain session
306        self.browser.open(self.container_path)
307        self.browser.getControl(name="searchtype").value = ['current_session']
308        self.browser.getControl(name="searchterm").value = '2004'
309        self.browser.getControl("Find student(s)").click()
310        self.assertTrue('Anna Tester' in self.browser.contents)
311        # Session fileds require integer values
312        self.browser.open(self.container_path)
313        self.browser.getControl(name="searchtype").value = ['current_session']
314        self.browser.getControl(name="searchterm").value = '2004/2005'
315        self.browser.getControl("Find student(s)").click()
316        self.assertTrue('Only year dates allowed' in self.browser.contents)
317        self.browser.open(self.manage_container_path)
318        self.browser.getControl(name="searchtype").value = ['current_session']
319        self.browser.getControl(name="searchterm").value = '2004/2005'
320        self.browser.getControl("Find student(s)").click()
321        self.assertTrue('Only year dates allowed' in self.browser.contents)
322
323        # We can find a student in a certain study_mode
324        self.browser.open(self.container_path)
325        self.browser.getControl(name="searchtype").value = ['current_mode']
326        self.browser.getControl(name="searchterm").value = 'ug_ft'
327        self.browser.getControl("Find student(s)").click()
328        self.assertTrue('Anna Tester' in self.browser.contents)
329
330        # We can find a student in a certain department
331        self.browser.open(self.container_path)
332        self.browser.getControl(name="searchtype").value = ['depcode']
333        self.browser.getControl(name="searchterm").value = 'dep1'
334        self.browser.getControl("Find student(s)").click()
335        self.assertTrue('Anna Tester' in self.browser.contents)
336
337        # We can find a student by searching for all kind of name parts
338        self.browser.open(self.manage_container_path)
339        self.browser.getControl("Find student(s)").click()
340        self.assertTrue('Empty search string' in self.browser.contents)
341        self.browser.getControl(name="searchtype").value = ['fullname']
342        self.browser.getControl(name="searchterm").value = 'Anna Tester'
343        self.browser.getControl("Find student(s)").click()
344        self.assertTrue('Anna Tester' in self.browser.contents)
345        self.browser.open(self.manage_container_path)
346        self.browser.getControl(name="searchtype").value = ['fullname']
347        self.browser.getControl(name="searchterm").value = 'Anna'
348        self.browser.getControl("Find student(s)").click()
349        self.assertTrue('Anna Tester' in self.browser.contents)
350        self.browser.open(self.manage_container_path)
351        self.browser.getControl(name="searchtype").value = ['fullname']
352        self.browser.getControl(name="searchterm").value = 'Tester'
353        self.browser.getControl("Find student(s)").click()
354        self.assertTrue('Anna Tester' in self.browser.contents)
355        self.browser.open(self.manage_container_path)
356        self.browser.getControl(name="searchtype").value = ['fullname']
357        self.browser.getControl(name="searchterm").value = 'An'
358        self.browser.getControl("Find student(s)").click()
359        self.assertFalse('Anna Tester' in self.browser.contents)
360        self.browser.open(self.manage_container_path)
361        self.browser.getControl(name="searchtype").value = ['fullname']
362        self.browser.getControl(name="searchterm").value = 'An*'
363        self.browser.getControl("Find student(s)").click()
364        self.assertTrue('Anna Tester' in self.browser.contents)
365        self.browser.open(self.manage_container_path)
366        self.browser.getControl(name="searchtype").value = ['fullname']
367        self.browser.getControl(name="searchterm").value = 'tester'
368        self.browser.getControl("Find student(s)").click()
369        self.assertTrue('Anna Tester' in self.browser.contents)
370        self.browser.open(self.manage_container_path)
371        self.browser.getControl(name="searchtype").value = ['fullname']
372        self.browser.getControl(name="searchterm").value = 'Tester Ana'
373        self.browser.getControl("Find student(s)").click()
374        self.assertFalse('Anna Tester' in self.browser.contents)
375        self.browser.open(self.manage_container_path)
376        self.browser.getControl(name="searchtype").value = ['fullname']
377        self.browser.getControl(name="searchterm").value = 'Tester Anna'
378        self.browser.getControl("Find student(s)").click()
379        self.assertTrue('Anna Tester' in self.browser.contents)
380        # The old searchterm will be used again
381        self.browser.getControl("Find student(s)").click()
382        self.assertTrue('Anna Tester' in self.browser.contents)
383
384        # We can find suspended students
385        self.student.suspended = True
386        notify(grok.ObjectModifiedEvent(self.student))
387        self.browser.open(self.manage_container_path)
388        self.browser.getControl(name="searchtype").value = ['suspended']
389        self.browser.getControl("Find student(s)").click()
390        self.assertTrue('Anna Tester' in self.browser.contents)
391        self.browser.open(self.container_path)
392        self.browser.getControl(name="searchtype").value = ['suspended']
393        self.browser.getControl("Find student(s)").click()
394        self.assertTrue('Anna Tester' in self.browser.contents)
395
396        # The catalog is informed when studycourse objects have been
397        # edited
398        self.browser.open(self.studycourse_path + '/manage')
399        self.browser.getControl(name="form.current_session").value = ['2010']
400        self.browser.getControl(name="form.entry_session").value = ['2010']
401        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
402        self.browser.getControl("Save").click()
403
404        # We can find the student in the new session
405        self.browser.open(self.manage_container_path)
406        self.browser.getControl(name="searchtype").value = ['current_session']
407        self.browser.getControl(name="searchterm").value = '2010'
408        self.browser.getControl("Find student(s)").click()
409        self.assertTrue('Anna Tester' in self.browser.contents)
410
411        ctrl = self.browser.getControl(name='entries')
412        ctrl.getControl(value=self.student_id).selected = True
413        self.browser.getControl("Remove selected", index=0).click()
414        self.assertTrue('Successfully removed' in self.browser.contents)
415        self.browser.getControl(name="searchtype").value = ['student_id']
416        self.browser.getControl(name="searchterm").value = self.student_id
417        self.browser.getControl("Find student(s)").click()
418        self.assertTrue('No student found' in self.browser.contents)
419
420        self.browser.open(self.container_path)
421        self.browser.getControl(name="searchtype").value = ['student_id']
422        self.browser.getControl(name="searchterm").value = self.student_id
423        self.browser.getControl("Find student(s)").click()
424        self.assertTrue('No student found' in self.browser.contents)
425        return
426
427class OfficerUITests(StudentsFullSetup):
428    # Tests for Student class views and pages
429
430    def test_student_properties(self):
431        self.student['studycourse'].current_level = 100
432        self.assertEqual(self.student.current_level, 100)
433        self.student['studycourse'].current_session = 2011
434        self.assertEqual(self.student.current_session, 2011)
435        self.student['studycourse'].current_verdict = 'A'
436        self.assertEqual(self.student.current_verdict, 'A')
437        return
438
439    def test_studylevelmanagepage(self):
440        studylevel = StudentStudyLevel()
441        studylevel.level = 100
442        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
443        self.student['studycourse'].addStudentStudyLevel(
444            cert,studylevel)
445        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
446        self.browser.open(self.studycourse_path + '/100/manage')
447        self.assertEqual(self.browser.url, self.studycourse_path + '/100/manage')
448        self.assertEqual(self.browser.headers['Status'], '200 Ok')
449
450    def test_basic_auth(self):
451        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
452        self.browser.open('http://localhost/app')
453        self.browser.getLink("Logout").click()
454        self.assertTrue('You have been logged out' in self.browser.contents)
455        # But we are still logged in since we've used basic authentication here.
456        # Wikipedia says: Existing browsers retain authentication information
457        # until the tab or browser is closed or the user clears the history.
458        # HTTP does not provide a method for a server to direct clients to
459        # discard these cached credentials. This means that there is no
460        # effective way for a server to "log out" the user without closing
461        # the browser. This is a significant defect that requires browser
462        # manufacturers to support a "logout" user interface element ...
463        self.assertTrue('Manager' in self.browser.contents)
464
465    def test_basic_auth_base64(self):
466        auth_token = base64.b64encode('mgr:mgrpw')
467        self.browser.addHeader('Authorization', 'Basic %s' % auth_token)
468        self.browser.open(self.manage_container_path)
469        self.assertEqual(self.browser.headers['Status'], '200 Ok')
470
471    def test_manage_access(self):
472        # Managers can access the pages of students
473        # and can perform actions
474        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
475        self.browser.open(self.student_path)
476        self.assertEqual(self.browser.headers['Status'], '200 Ok')
477        self.assertEqual(self.browser.url, self.student_path)
478        self.browser.getLink("Trigger").click()
479        self.assertEqual(self.browser.headers['Status'], '200 Ok')
480        # Managers can trigger transitions
481        self.browser.getControl(name="transition").value = ['admit']
482        self.browser.getControl("Save").click()
483        # Managers can edit base
484        self.browser.open(self.student_path)
485        self.browser.getLink("Manage").click()
486        self.assertEqual(self.browser.url, self.manage_student_path)
487        self.assertEqual(self.browser.headers['Status'], '200 Ok')
488        self.browser.getControl(name="form.firstname").value = 'John'
489        self.browser.getControl(name="form.lastname").value = 'Tester'
490        self.browser.getControl(name="form.reg_number").value = '345'
491        self.browser.getControl(name="password").value = 'secret'
492        self.browser.getControl(name="control_password").value = 'secret'
493        self.browser.getControl("Save").click()
494        self.assertMatches('...Form has been saved...',
495                           self.browser.contents)
496        self.browser.open(self.student_path)
497        self.browser.getLink("Clearance Data").click()
498        self.assertEqual(self.browser.headers['Status'], '200 Ok')
499        self.assertEqual(self.browser.url, self.clearance_path)
500        self.browser.getLink("Manage").click()
501        self.assertEqual(self.browser.headers['Status'], '200 Ok')
502        self.assertEqual(self.browser.url, self.manage_clearance_path)
503        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
504        self.browser.getControl("Save").click()
505        self.assertMatches('...Form has been saved...',
506                           self.browser.contents)
507
508        self.browser.open(self.student_path)
509        self.browser.getLink("Personal Data").click()
510        self.assertEqual(self.browser.headers['Status'], '200 Ok')
511        self.assertEqual(self.browser.url, self.personal_path)
512        self.browser.getLink("Manage").click()
513        self.assertEqual(self.browser.headers['Status'], '200 Ok')
514        self.assertEqual(self.browser.url, self.manage_personal_path)
515        self.browser.open(self.personal_path)
516        self.assertTrue('Updated' in self.browser.contents)
517        self.browser.getLink("Edit").click()
518        self.assertEqual(self.browser.headers['Status'], '200 Ok')
519        self.assertEqual(self.browser.url, self.edit_personal_path)
520        self.browser.getControl("Save").click()
521        # perm_address is required in IStudentPersonalEdit
522        self.assertMatches('...Required input is missing...',
523                           self.browser.contents)
524        self.browser.getControl(name="form.perm_address").value = 'My address!'
525        self.browser.getControl("Save").click()
526        self.assertMatches('...Form has been saved...',
527                           self.browser.contents)
528
529        # Managers can browse all subobjects
530        self.browser.open(self.student_path)
531        self.browser.getLink("Payments").click()
532        self.assertEqual(self.browser.headers['Status'], '200 Ok')
533        self.assertEqual(self.browser.url, self.payments_path)
534        self.browser.open(self.student_path)
535        self.browser.getLink("Accommodation").click()
536        self.assertEqual(self.browser.headers['Status'], '200 Ok')
537        self.assertEqual(self.browser.url, self.acco_path)
538        self.browser.open(self.student_path)
539        self.browser.getLink("History").click()
540        self.assertEqual(self.browser.headers['Status'], '200 Ok')
541        self.assertEqual(self.browser.url, self.history_path)
542        self.assertMatches('...Admitted by Manager...',
543                           self.browser.contents)
544        # Only the Application Slip does not exist
545        self.assertFalse('Application Slip' in self.browser.contents)
546        return
547
548    def test_flash_notice(self):
549        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
550        self.browser.open(self.student_path)
551        self.assertFalse('alert alert-warning' in self.browser.contents)
552        self.student.flash_notice = u'Happy Birthday!'
553        self.browser.open(self.student_path)
554        self.assertTrue(
555            '<div><div class="alert alert-warning">Happy Birthday!</div>'
556            in self.browser.contents)
557        return
558
559    def test_manage_contact_student(self):
560        # Managers can contact student
561        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
562        # Remove required FieldProperty attribute first ...
563        delattr(Student, 'email')
564        # ... and replace by None
565        self.student.email = None
566        # Now we have to add  the FieldProperty attribute again. Otherwise
567        # many other tests below will fail.
568        iface = list(implementedBy(Student))[0]
569        field_property = FieldProperty(iface['email'])
570        setattr(Student, 'email', field_property)
571        self.browser.open(self.student_path)
572        self.browser.getLink("Send email").click()
573        self.browser.getControl(name="form.subject").value = 'Important subject'
574        self.browser.getControl(name="form.body").value = 'Hello!'
575        self.browser.getControl("Send message now").click()
576        self.assertTrue('An smtp server error occurred' in self.browser.contents)
577        self.student.email = 'xx@yy.zz'
578        self.browser.getControl("Send message now").click()
579        self.assertTrue('Your message has been sent' in self.browser.contents)
580        return
581
582    def test_manage_remove_department(self):
583        # Lazy student is studying CERT1
584        lazystudent = Student()
585        lazystudent.firstname = u'Lazy'
586        lazystudent.lastname = u'Student'
587        self.app['students'].addStudent(lazystudent)
588        student_id = lazystudent.student_id
589        student_path = self.container_path + '/' + student_id
590        lazystudent['studycourse'].certificate = self.certificate
591        notify(grok.ObjectModifiedEvent(lazystudent))
592        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
593        self.browser.open(student_path + '/studycourse')
594        self.assertTrue('CERT1' in self.browser.contents)
595        # After some years the department is removed
596        del self.app['faculties']['fac1']['dep1']
597        # So CERT1 does no longer exist and lazy student's
598        # certificate reference is removed too
599        self.browser.open(student_path + '/studycourse')
600        self.assertEqual(self.browser.headers['Status'], '200 Ok')
601        self.assertEqual(self.browser.url, student_path + '/studycourse')
602        self.assertFalse('CERT1' in self.browser.contents)
603        self.assertMatches('...<div>--</div>...',
604                           self.browser.contents)
605
606    def test_manage_upload_file(self):
607        # Managers can upload a file via the StudentClearanceManageFormPage
608        # The image is stored even if form has errors
609        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
610        self.browser.open(self.manage_clearance_path)
611        # No birth certificate has been uploaded yet
612        # Browsing the link shows a placerholder image
613        self.browser.open('birth_certificate')
614        self.assertEqual(
615            self.browser.headers['content-type'], 'image/jpeg')
616        self.assertEqual(len(self.browser.contents), PH_LEN)
617        # Create a pseudo image file and select it to be uploaded in form
618        # as birth certificate
619        self.browser.open(self.manage_clearance_path)
620        image = open(SAMPLE_IMAGE, 'rb')
621        ctrl = self.browser.getControl(name='birthcertificateupload')
622        file_ctrl = ctrl.mech_control
623        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
624        # The Save action does not upload files
625        self.browser.getControl("Save").click() # submit form
626        self.assertFalse(
627            '<a target="image" href="birth_certificate">'
628            in self.browser.contents)
629        # ... but the correct upload submit button does
630        image = open(SAMPLE_IMAGE)
631        ctrl = self.browser.getControl(name='birthcertificateupload')
632        file_ctrl = ctrl.mech_control
633        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
634        self.browser.getControl(
635            name='upload_birthcertificateupload').click()
636        # There is a correct <img> link included
637        self.assertTrue(
638            'href="http://localhost/app/students/K1000000/birth_certificate"'
639            in self.browser.contents)
640        # Browsing the link shows a real image
641        self.browser.open('birth_certificate')
642        self.assertEqual(
643            self.browser.headers['content-type'], 'image/jpeg')
644        self.assertEqual(len(self.browser.contents), 2787)
645        # We can't reupload a file. The existing file must be deleted first.
646        self.browser.open(self.manage_clearance_path)
647        self.assertFalse(
648            'upload_birthcertificateupload' in self.browser.contents)
649        # File must be deleted first
650        self.browser.getControl(name='delete_birthcertificateupload').click()
651        self.assertTrue(
652            'birth_certificate deleted' in self.browser.contents)
653        # Uploading a file which is bigger than 150k will raise an error
654        big_image = StringIO(open(SAMPLE_IMAGE, 'rb').read() * 75)
655        ctrl = self.browser.getControl(name='birthcertificateupload')
656        file_ctrl = ctrl.mech_control
657        file_ctrl.add_file(big_image, filename='my_birth_certificate.jpg')
658        self.browser.getControl(
659            name='upload_birthcertificateupload').click()
660        self.assertTrue(
661            'Uploaded file is too big' in self.browser.contents)
662        # we do not rely on filename extensions given by uploaders
663        image = open(SAMPLE_IMAGE, 'rb') # a jpg-file
664        ctrl = self.browser.getControl(name='birthcertificateupload')
665        file_ctrl = ctrl.mech_control
666        # tell uploaded file is bmp
667        file_ctrl.add_file(image, filename='my_birth_certificate.bmp')
668        self.browser.getControl(
669            name='upload_birthcertificateupload').click()
670        self.assertTrue(
671            # jpg file was recognized
672            'File birth_certificate.jpg uploaded.' in self.browser.contents)
673        # Delete file again
674        self.browser.getControl(name='delete_birthcertificateupload').click()
675        self.assertTrue(
676            'birth_certificate deleted' in self.browser.contents)
677        # File names must meet several conditions
678        bmp_image = open(SAMPLE_IMAGE_BMP, 'rb')
679        ctrl = self.browser.getControl(name='birthcertificateupload')
680        file_ctrl = ctrl.mech_control
681        file_ctrl.add_file(bmp_image, filename='my_birth_certificate.bmp')
682        self.browser.getControl(
683            name='upload_birthcertificateupload').click()
684        self.assertTrue('Only the following extensions are allowed'
685            in self.browser.contents)
686
687        # Managers can upload a file via the StudentBaseManageFormPage
688        self.browser.open(self.manage_student_path)
689        image = open(SAMPLE_IMAGE_BMP, 'rb')
690        ctrl = self.browser.getControl(name='passportuploadmanage')
691        file_ctrl = ctrl.mech_control
692        file_ctrl.add_file(image, filename='my_photo.bmp')
693        self.browser.getControl(
694            name='upload_passportuploadmanage').click()
695        self.assertTrue('jpg file format expected'
696            in self.browser.contents)
697        ctrl = self.browser.getControl(name='passportuploadmanage')
698        file_ctrl = ctrl.mech_control
699        image = open(SAMPLE_IMAGE, 'rb')
700        file_ctrl.add_file(image, filename='my_photo.jpg')
701        self.browser.getControl(
702            name='upload_passportuploadmanage').click()
703        self.assertTrue(
704            'src="http://localhost/app/students/K1000000/passport.jpg"'
705            in self.browser.contents)
706        # We remove the passport file again
707        self.browser.open(self.manage_student_path)
708        self.browser.getControl('Delete').click()
709        self.browser.open(self.student_path + '/clearance_slip.pdf')
710        self.assertEqual(self.browser.headers['Status'], '200 Ok')
711        self.assertEqual(self.browser.headers['Content-Type'],
712                         'application/pdf')
713        # We want to see the signature fields.
714        IWorkflowState(self.student).setState('cleared')
715        self.browser.open(self.student_path + '/clearance_slip.pdf')
716        self.assertEqual(self.browser.headers['Status'], '200 Ok')
717        self.assertEqual(self.browser.headers['Content-Type'],
718                         'application/pdf')
719        path = os.path.join(samples_dir(), 'clearance_slip.pdf')
720        open(path, 'wb').write(self.browser.contents)
721        print "Sample PDF clearance_slip.pdf written to %s" % path
722
723    def test_manage_course_lists(self):
724        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
725        self.browser.open(self.student_path)
726        self.browser.getLink("Study Course").click()
727        self.assertEqual(self.browser.headers['Status'], '200 Ok')
728        self.assertEqual(self.browser.url, self.studycourse_path)
729        self.assertTrue('Undergraduate Full-Time' in self.browser.contents)
730        self.browser.getLink("Manage").click()
731        self.assertTrue('Manage study course' in self.browser.contents)
732        # Before we can select a level, the certificate must
733        # be selected and saved
734        self.browser.getControl(name="form.certificate").value = ['CERT1']
735        self.browser.getControl(name="form.current_session").value = ['2004']
736        self.browser.getControl(name="form.current_verdict").value = ['A']
737        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
738        self.browser.getControl("Save").click()
739        # Now we can save also the current level which depends on start and end
740        # level of the certificate
741        self.browser.getControl(name="form.current_level").value = ['100']
742        self.browser.getControl("Save").click()
743        # Managers can add and remove any study level (course list)
744        self.browser.getControl(name="addlevel").value = ['100']
745        self.browser.getControl("Add study level").click()
746        self.assertMatches(
747            '...You must select a session...', self.browser.contents)
748        self.browser.getControl(name="addlevel").value = ['100']
749        self.browser.getControl(name="level_session").value = ['2004']
750        self.browser.getControl("Add study level").click()
751        self.assertMatches('...<span>100</span>...', self.browser.contents)
752        self.assertEqual(self.student['studycourse']['100'].level, 100)
753        self.assertEqual(self.student['studycourse']['100'].level_session, 2004)
754        self.browser.getControl(name="addlevel").value = ['100']
755        self.browser.getControl(name="level_session").value = ['2004']
756        self.browser.getControl("Add study level").click()
757        self.assertMatches('...This level exists...', self.browser.contents)
758        self.browser.getControl("Remove selected").click()
759        self.assertMatches(
760            '...No study level selected...', self.browser.contents)
761        self.browser.getControl(name="val_id").value = ['100']
762        self.browser.getControl(name="level_session").value = ['2004']
763        self.browser.getControl("Remove selected").click()
764        self.assertMatches('...Successfully removed...', self.browser.contents)
765        # Removing levels is properly logged
766        logfile = os.path.join(
767            self.app['datacenter'].storage, 'logs', 'students.log')
768        logcontent = open(logfile).read()
769        self.assertTrue('zope.mgr - students.browser.StudyCourseManageFormPage '
770                        '- K1000000 - removed: 100' in logcontent)
771        # Add level again
772        self.browser.getControl(name="addlevel").value = ['100']
773        self.browser.getControl(name="level_session").value = ['2004']
774        self.browser.getControl("Add study level").click()
775
776        # Managers can view and manage course lists
777        self.browser.getLink("100").click()
778        self.assertMatches(
779            '...: Study Level 100 (Year 1)...', self.browser.contents)
780        self.browser.getLink("Manage").click()
781        self.browser.getControl(name="form.level_session").value = ['2002']
782        self.browser.getControl("Save").click()
783        self.browser.getControl("Remove selected").click()
784        self.assertMatches('...No ticket selected...', self.browser.contents)
785        ctrl = self.browser.getControl(name='val_id')
786        ctrl.getControl(value='COURSE1').selected = True
787        self.browser.getControl("Remove selected", index=0).click()
788        self.assertTrue('Successfully removed' in self.browser.contents)
789        # Removing course tickets is properly logged
790        logfile = os.path.join(
791            self.app['datacenter'].storage, 'logs', 'students.log')
792        logcontent = open(logfile).read()
793        self.assertTrue('zope.mgr - students.browser.StudyLevelManageFormPage '
794        '- K1000000 - removed: COURSE1 at 100' in logcontent)
795        self.browser.getLink("here").click()
796        self.browser.getControl(name="form.course").value = ['COURSE1']
797        self.course.credits = 100
798        self.browser.getControl("Add course ticket").click()
799        self.assertMatches(
800            '...Total credits exceed 50...', self.browser.contents)
801        self.course.credits = 10
802        self.browser.getControl("Add course ticket").click()
803        self.assertTrue('Successfully added' in self.browser.contents)
804        # We can do the same by adding the course on the manage page directly
805        del self.student['studycourse']['100']['COURSE1']
806        self.browser.getControl(name="course").value = 'COURSE1'
807        self.browser.getControl("Add course ticket").click()
808        self.assertTrue('Successfully added' in self.browser.contents)
809        self.browser.getLink("here").click()
810        self.browser.getControl(name="form.course").value = ['COURSE1']
811        self.browser.getControl("Add course ticket").click()
812        self.assertTrue('The ticket exists' in self.browser.contents)
813        self.browser.getControl("Cancel").click()
814        self.browser.getLink("COURSE1").click()
815        self.browser.getLink("Manage").click()
816        self.browser.getControl("Save").click()
817        self.assertTrue('Form has been saved' in self.browser.contents)
818        # Grade and weight have been determined
819        self.browser.open(self.studycourse_path + '/100/COURSE1')
820        self.assertFalse('Grade' in self.browser.contents)
821        self.assertFalse('Weight' in self.browser.contents)
822        self.student['studycourse']['100']['COURSE1'].score = 55
823        self.browser.open(self.studycourse_path + '/100/COURSE1')
824        self.assertTrue('Grade' in self.browser.contents)
825        self.assertTrue('Weight' in self.browser.contents)
826        self.assertEqual(self.student['studycourse']['100']['COURSE1'].grade, 'C')
827        self.assertEqual(self.student['studycourse']['100']['COURSE1'].weight, 3)
828        # We add another ticket to check if GPA will be correctly calculated
829        # (and rounded)
830        courseticket = createObject('waeup.CourseTicket')
831        courseticket.code = 'ANYCODE'
832        courseticket.title = u'Any TITLE'
833        courseticket.credits = 13
834        courseticket.score = 66
835        courseticket.semester = 1
836        courseticket.dcode = u'ANYDCODE'
837        courseticket.fcode = u'ANYFCODE'
838        self.student['studycourse']['100']['COURSE2'] = courseticket
839        self.browser.open(self.student_path + '/studycourse/100')
840        # total credits
841        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[1], 23)
842        # weigheted credits = 3 * 10 + 4 * 13
843        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[2], 82.0)
844        # sgpa = 82 / 23
845        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 3.565)
846        # Carry-over courses will be collected when next level is created
847        self.browser.open(self.student_path + '/studycourse/manage')
848        # Add next level
849        self.student['studycourse']['100']['COURSE1'].score = 10
850        self.browser.getControl(name="addlevel").value = ['200']
851        self.browser.getControl(name="level_session").value = ['2005']
852        self.browser.getControl("Add study level").click()
853        self.browser.getLink("200").click()
854        self.assertMatches(
855            '...: Study Level 200 (Year 2)...', self.browser.contents)
856        # Since COURSE1 has score 10 it becomes a carry-over course
857        # in level 200
858        self.assertEqual(
859            sorted(self.student['studycourse']['200'].keys()), [u'COURSE1'])
860        self.assertTrue(
861            self.student['studycourse']['200']['COURSE1'].carry_over)
862        # Passed and failed courses have been counted
863        self.assertEqual(
864            self.student['studycourse']['100'].passed_params,
865            (1, 1, 13, 10, ['COURSE1']))
866        self.assertEqual(
867            self.student['studycourse']['200'].passed_params,
868            (0, 0, 0, 0, []))
869        # And also cumulative params can be calculated. Meanwhile we have the
870        # following courses: COURSE1 and COURSE2 in level 100 and
871        # COURSE1 as carry-over course in level 200.
872        self.assertEqual(
873            self.student['studycourse']['100'].cumulative_params,
874            (2.261, 23, 52.0, 23, 13))
875        # COURSE1 in level 200 is not taken into consideration
876        # when calculating the gpa.
877        self.assertEqual(
878            self.student['studycourse']['200'].cumulative_params,
879            (2.261, 23, 52.0, 33, 13))
880        return
881
882    def test_gpa_calculation_with_carryover(self):
883        studylevel = createObject(u'waeup.StudentStudyLevel')
884        studylevel.level = 100
885        studylevel.level_session = 2005
886        self.student['studycourse'].entry_mode = 'ug_ft'
887        self.student['studycourse'].addStudentStudyLevel(
888            self.certificate, studylevel)
889        # First course has been added automatically.
890        # Set score above passmark.
891        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark + 1
892        # GPA is 1.
893        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 1.0)
894        # Set score below passmark.
895        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark - 1
896        # GPA is still 0.
897        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 0.0)
898        studylevel2 = createObject(u'waeup.StudentStudyLevel')
899        studylevel2.level = 200
900        studylevel2.level_session = 2006
901        self.student['studycourse'].addStudentStudyLevel(
902            self.certificate, studylevel2)
903        # Carry-over course has been autonatically added.
904        studylevel2['COURSE1'].score = 66
905        # The score of the carry-over course is now used for calculation of the
906        # GPA at level 100 ...
907        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 4.0)
908        # ... but not at level 200
909        self.assertEqual(self.student['studycourse']['200'].gpa_params_rectified[0], 0.0)
910        return
911
912    def test_manage_payments(self):
913        # Managers can add online school fee payment tickets
914        # if certain requirements are met
915        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
916        self.browser.open(self.payments_path)
917        IWorkflowState(self.student).setState('cleared')
918        self.browser.getLink("Add current session payment ticket").click()
919        self.browser.getControl(name="form.p_category").value = ['schoolfee']
920        self.browser.getControl("Create ticket").click()
921        self.assertMatches('...ticket created...',
922                           self.browser.contents)
923        ctrl = self.browser.getControl(name='val_id')
924        value = ctrl.options[0]
925        self.browser.getLink(value).click()
926        self.assertMatches('...Amount Authorized...',
927                           self.browser.contents)
928        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
929        payment_url = self.browser.url
930        logfile = os.path.join(
931            self.app['datacenter'].storage, 'logs', 'students.log')
932        logcontent = open(logfile).read()
933        self.assertTrue(
934            ' zope.mgr - students.browser.OnlinePaymentAddFormPage - '
935            'K1000000 - added: %s' % value
936            in logcontent)
937        # The pdf payment slip can't yet be opened
938        #self.browser.open(payment_url + '/payment_slip.pdf')
939        #self.assertMatches('...Ticket not yet paid...',
940        #                   self.browser.contents)
941
942        # The same payment (with same p_item, p_session and p_category)
943        # can be initialized a second time if the former ticket is not yet paid.
944        self.browser.open(self.payments_path)
945        self.browser.getLink("Add current session payment ticket").click()
946        self.browser.getControl(name="form.p_category").value = ['schoolfee']
947        self.browser.getControl("Create ticket").click()
948        self.assertMatches('...Payment ticket created...',
949                           self.browser.contents)
950
951        # The ticket can be found in the payments_catalog
952        cat = queryUtility(ICatalog, name='payments_catalog')
953        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
954        self.assertEqual(len(results), 2)
955        self.assertTrue(results[0] is self.student['payments'][value])
956        # Managers can approve the payment
957        # If, by some reason, the state has already changed,
958        # an access code is created after approval.
959        IWorkflowState(self.student).setState('school fee paid')
960        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
961        self.browser.open(payment_url)
962        self.browser.getLink("Approve payment").click()
963        self.assertMatches('...Payment approved...',
964                          self.browser.contents)
965        # Approval is logged in students.log ...
966        logcontent = open(logfile).read()
967        self.assertTrue(
968            'zope.mgr - students.browser.OnlinePaymentApproveView '
969            '- K1000000 - schoolfee payment approved'
970            in logcontent)
971        # ... and in payments.log
972        logfile = os.path.join(
973            self.app['datacenter'].storage, 'logs', 'payments.log')
974        logcontent = open(logfile).read()
975        self.assertTrue(
976            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
977            in logcontent)
978        # The authorized amount has been stored in the new access code
979        self.assertEqual(
980            self.app['accesscodes']['SFE-0'].values()[0].cost,40000.0)
981
982        # The catalog has been updated
983        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
984        self.assertTrue(len(results), 0)
985        results = list(cat.searchResults(p_state=('paid', 'paid')))
986        self.assertTrue(len(results), 1)
987        self.assertTrue(results[0] is self.student['payments'][value])
988
989        # Payments can't be approved twice
990        self.browser.open(payment_url + '/approve')
991        self.assertMatches('...This ticket has already been paid...',
992                          self.browser.contents)
993
994        # Now the first ticket is paid and no more ticket of same type
995        # (with same p_item, p_session and p_category) can be added.
996        # First we have to reset the workflow state.
997        IWorkflowState(self.student).setState('cleared')
998        self.browser.open(self.payments_path)
999        self.browser.getLink("Add current session payment ticket").click()
1000        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1001        self.browser.getControl("Create ticket").click()
1002        self.assertMatches(
1003            '...This type of payment has already been made...',
1004            self.browser.contents)
1005
1006        # Managers can open the pdf payment slip
1007        self.browser.open(payment_url)
1008        self.browser.getLink("Download payment slip").click()
1009        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1010        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
1011
1012        # Managers can remove online school fee payment tickets
1013        self.browser.open(self.payments_path)
1014        self.browser.getControl("Remove selected").click()
1015        self.assertMatches('...No payment selected...', self.browser.contents)
1016        ctrl = self.browser.getControl(name='val_id')
1017        value = ctrl.options[0]
1018        ctrl.getControl(value=value).selected = True
1019        self.browser.getControl("Remove selected", index=0).click()
1020        self.assertTrue('Successfully removed' in self.browser.contents)
1021
1022        # Managers can add online clearance payment tickets
1023        self.browser.open(self.payments_path + '/addop')
1024        self.browser.getControl(name="form.p_category").value = ['clearance']
1025        self.browser.getControl("Create ticket").click()
1026        self.assertMatches('...ticket created...',
1027                           self.browser.contents)
1028
1029        # Managers can approve the payment
1030        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
1031        ctrl = self.browser.getControl(name='val_id')
1032        value = ctrl.options[1] # The clearance payment is the second in the table
1033        self.browser.getLink(value).click()
1034        self.browser.open(self.browser.url + '/approve')
1035        self.assertMatches('...Payment approved...',
1036                          self.browser.contents)
1037        expected = '''...
1038        <td>
1039          <span>Paid</span>
1040        </td>...'''
1041        self.assertMatches(expected,self.browser.contents)
1042        # The new CLR-0 pin has been created
1043        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
1044        pin = self.app['accesscodes']['CLR-0'].keys()[0]
1045        ac = self.app['accesscodes']['CLR-0'][pin]
1046        self.assertEqual(ac.owner, self.student_id)
1047        self.assertEqual(ac.cost, 3456.0)
1048
1049        # Managers can add online transcript payment tickets
1050        self.browser.open(self.payments_path + '/addop')
1051        self.browser.getControl(name="form.p_category").value = ['transcript']
1052        self.browser.getControl("Create ticket").click()
1053        self.assertMatches('...ticket created...',
1054                           self.browser.contents)
1055
1056        # Managers can approve the payment
1057        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
1058        ctrl = self.browser.getControl(name='val_id')
1059        value = ctrl.options[2] # The clearance payment is the third in the table
1060        self.browser.getLink(value).click()
1061        self.browser.open(self.browser.url + '/approve')
1062        self.assertMatches('...Payment approved...',
1063                          self.browser.contents)
1064        expected = '''...
1065        <td>
1066          <span>Paid</span>
1067        </td>...'''
1068        self.assertMatches(expected,self.browser.contents)
1069        # The new CLR-0 pin has been created
1070        self.assertEqual(len(self.app['accesscodes']['TSC-0']),1)
1071        pin = self.app['accesscodes']['TSC-0'].keys()[0]
1072        ac = self.app['accesscodes']['TSC-0'][pin]
1073        self.assertEqual(ac.owner, self.student_id)
1074        self.assertEqual(ac.cost, 4567.0)
1075        return
1076
1077    def test_add_transfer_payment(self):
1078        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1079        self.browser.open(self.payments_path)
1080        self.browser.getLink("Add current session payment ticket").click()
1081        self.browser.getControl(name="form.p_category").value = ['transfer']
1082        self.browser.getControl(name="new_programme").value = 'my new study course'
1083        self.browser.getControl("Create ticket").click()
1084        self.assertMatches('...ticket created...',
1085                           self.browser.contents)
1086        ctrl = self.browser.getControl(name='val_id')
1087        value = ctrl.options[0]
1088        self.browser.getLink(value).click()
1089        self.assertMatches('...my new study course...',
1090                           self.browser.contents)
1091        self.assertEqual(self.student['payments'][value].p_item, u'my new study course')
1092
1093    def test_manage_payments_bypass_ac_creation(self):
1094        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1095        self.browser.open(self.payments_path)
1096        IWorkflowState(self.student).setState('cleared')
1097        self.browser.getLink("Add current session payment ticket").click()
1098        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1099        self.browser.getControl("Create ticket").click()
1100        ctrl = self.browser.getControl(name='val_id')
1101        value = ctrl.options[0]
1102        self.browser.getLink(value).click()
1103        payment_url = self.browser.url
1104        logfile = os.path.join(
1105            self.app['datacenter'].storage, 'logs', 'students.log')
1106        # The ticket can be found in the payments_catalog
1107        cat = queryUtility(ICatalog, name='payments_catalog')
1108        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
1109        self.assertTrue(len(results), 1)
1110        self.assertTrue(results[0] is self.student['payments'][value])
1111        # Managers can approve the payment
1112        self.browser.open(payment_url)
1113        self.browser.getLink("Approve payment").click()
1114        self.assertMatches('...Payment approved...',
1115                          self.browser.contents)
1116        # Approval is logged in students.log ...
1117        logcontent = open(logfile).read()
1118        self.assertTrue(
1119            'zope.mgr - students.browser.OnlinePaymentApproveView '
1120            '- K1000000 - schoolfee payment approved'
1121            in logcontent)
1122        # ... and in payments.log
1123        logfile = os.path.join(
1124            self.app['datacenter'].storage, 'logs', 'payments.log')
1125        logcontent = open(logfile).read()
1126        self.assertTrue(
1127            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
1128            in logcontent)
1129        # Student is in state school fee paid, no activation
1130        # code was necessary.
1131        self.assertEqual(self.student.state, 'school fee paid')
1132        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
1133        return
1134
1135    def test_payment_disabled(self):
1136        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1137        self.browser.open(self.payments_path)
1138        IWorkflowState(self.student).setState('cleared')
1139        self.browser.getLink("Add current session payment ticket").click()
1140        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1141        self.browser.getControl("Create ticket").click()
1142        self.assertMatches('...ticket created...',
1143                           self.browser.contents)
1144        self.app['configuration']['2004'].payment_disabled = ['sf_all']
1145        self.browser.getLink("Add current session payment ticket").click()
1146        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1147        self.browser.getControl("Create ticket").click()
1148        self.assertMatches('...Payment temporarily disabled...',
1149                           self.browser.contents)
1150        return
1151
1152    def test_manage_balance_payments(self):
1153
1154        # Login
1155        #self.browser.open(self.login_path)
1156        #self.browser.getControl(name="form.login").value = self.student_id
1157        #self.browser.getControl(name="form.password").value = 'spwd'
1158        #self.browser.getControl("Login").click()
1159
1160        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1161        self.browser.open(self.payments_path)
1162
1163        # Managers can add previous school fee payment tickets in any state.
1164        IWorkflowState(self.student).setState('courses registered')
1165        self.browser.open(self.payments_path)
1166        self.browser.getLink("Add balance payment ticket").click()
1167
1168        # Previous session payment form is provided
1169        self.assertEqual(self.student.current_session, 2004)
1170        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1171        self.browser.getControl(name="form.balance_session").value = ['2004']
1172        self.browser.getControl(name="form.balance_level").value = ['300']
1173        self.browser.getControl(name="form.balance_amount").value = '-567.8'
1174        self.browser.getControl("Create ticket").click()
1175        self.assertMatches('...Amount must be greater than 0...',
1176                           self.browser.contents)
1177        self.browser.getControl(name="form.balance_amount").value = '0'
1178        self.browser.getControl("Create ticket").click()
1179        self.assertMatches('...Amount must be greater than 0...',
1180                           self.browser.contents)
1181        self.browser.getControl(name="form.balance_amount").value = '567.8'
1182        self.browser.getControl("Create ticket").click()
1183        self.assertMatches('...ticket created...',
1184                           self.browser.contents)
1185        ctrl = self.browser.getControl(name='val_id')
1186        value = ctrl.options[0]
1187        self.browser.getLink(value).click()
1188        self.assertMatches('...Amount Authorized...',
1189                           self.browser.contents)
1190        self.assertEqual(self.student['payments'][value].amount_auth, 567.8)
1191        # Payment attributes are properly set
1192        self.assertEqual(self.student['payments'][value].p_session, 2004)
1193        self.assertEqual(self.student['payments'][value].p_level, 300)
1194        self.assertEqual(self.student['payments'][value].p_item, u'Balance')
1195        self.assertEqual(self.student['payments'][value].p_category, 'schoolfee')
1196        # Adding payment tickets is logged.
1197        logfile = os.path.join(
1198            self.app['datacenter'].storage, 'logs', 'students.log')
1199        logcontent = open(logfile).read()
1200        self.assertTrue('zope.mgr - students.browser.BalancePaymentAddFormPage '
1201                        '- K1000000 - added: %s' % value in logcontent)
1202
1203    def test_manage_accommodation(self):
1204        logfile = os.path.join(
1205            self.app['datacenter'].storage, 'logs', 'students.log')
1206        # Managers can add online booking fee payment tickets and open the
1207        # callback view (see test_manage_payments)
1208        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1209        self.browser.open(self.payments_path)
1210        self.browser.getLink("Add current session payment ticket").click()
1211        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
1212        # If student is not in accommodation session, payment cannot be processed
1213        self.app['hostels'].accommodation_session = 2011
1214        self.browser.getControl("Create ticket").click()
1215        self.assertMatches('...Your current session does not match...',
1216                           self.browser.contents)
1217        self.app['hostels'].accommodation_session = 2004
1218        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
1219        self.browser.getControl("Create ticket").click()
1220        ctrl = self.browser.getControl(name='val_id')
1221        value = ctrl.options[0]
1222        self.browser.getLink(value).click()
1223        self.browser.open(self.browser.url + '/approve')
1224        # The new HOS-0 pin has been created
1225        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
1226        pin = self.app['accesscodes']['HOS-0'].keys()[0]
1227        ac = self.app['accesscodes']['HOS-0'][pin]
1228        self.assertEqual(ac.owner, self.student_id)
1229        parts = pin.split('-')[1:]
1230        sfeseries, sfenumber = parts
1231        # Managers can use HOS code and book a bed space with it
1232        self.browser.open(self.acco_path)
1233        self.browser.getControl("Book accommodation").click()
1234        self.assertMatches('...You are in the wrong...',
1235                           self.browser.contents)
1236        IWorkflowInfo(self.student).fireTransition('admit')
1237        # An existing HOS code can only be used if students
1238        # are in accommodation session
1239        self.student['studycourse'].current_session = 2003
1240        self.browser.getControl("Book accommodation").click()
1241        self.assertMatches('...Your current session does not match...',
1242                           self.browser.contents)
1243        self.student['studycourse'].current_session = 2004
1244        # All requirements are met and ticket can be created
1245        self.browser.getControl("Book accommodation").click()
1246        self.assertMatches('...Activation Code:...',
1247                           self.browser.contents)
1248        self.browser.getControl(name="ac_series").value = sfeseries
1249        self.browser.getControl(name="ac_number").value = sfenumber
1250        self.browser.getControl("Create bed ticket").click()
1251        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
1252                           self.browser.contents)
1253        # Bed has been allocated
1254        bed1 = self.app['hostels']['hall-1']['hall-1_A_101_A']
1255        self.assertTrue(bed1.owner == self.student_id)
1256        # BedTicketAddPage is now blocked
1257        self.browser.getControl("Book accommodation").click()
1258        self.assertMatches('...You already booked a bed space...',
1259            self.browser.contents)
1260        # The bed ticket displays the data correctly
1261        self.browser.open(self.acco_path + '/2004')
1262        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
1263                           self.browser.contents)
1264        self.assertMatches('...2004/2005...', self.browser.contents)
1265        self.assertMatches('...regular_male_fr...', self.browser.contents)
1266        self.assertMatches('...%s...' % pin, self.browser.contents)
1267        # Booking is properly logged
1268        logcontent = open(logfile).read()
1269        self.assertTrue('zope.mgr - students.browser.BedTicketAddPage '
1270            '- K1000000 - booked: hall-1_A_101_A' in logcontent)
1271        # Managers can relocate students if the student's bed_type has changed
1272        self.browser.getLink("Relocate student").click()
1273        self.assertMatches(
1274            "...Student can't be relocated...", self.browser.contents)
1275        self.student.sex = u'f'
1276        self.browser.getLink("Relocate student").click()
1277        self.assertMatches(
1278            "...Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
1279        self.assertTrue(bed1.owner == NOT_OCCUPIED)
1280        bed2 = self.app['hostels']['hall-1']['hall-1_A_101_B']
1281        self.assertTrue(bed2.owner == self.student_id)
1282        self.assertTrue(self.student['accommodation'][
1283            '2004'].bed_type == u'regular_female_fr')
1284        # Relocation is properly logged
1285        logcontent = open(logfile).read()
1286        self.assertTrue('zope.mgr - students.accommodation.BedTicket '
1287            '- K1000000 - relocated: hall-1_A_101_B' in logcontent)
1288        # The payment object still shows the original payment item
1289        payment_id = self.student['payments'].keys()[0]
1290        payment = self.student['payments'][payment_id]
1291        self.assertTrue(payment.p_item == u'regular_male_fr')
1292        # Managers can relocate students if the bed's bed_type has changed
1293        bed1.bed_type = u'regular_female_fr'
1294        bed2.bed_type = u'regular_male_fr'
1295        notify(grok.ObjectModifiedEvent(bed1))
1296        notify(grok.ObjectModifiedEvent(bed2))
1297        self.browser.getLink("Relocate student").click()
1298        self.assertMatches(
1299            "...Student relocated...", self.browser.contents)
1300        self.assertMatches(
1301            "... Hall 1, Block A, Room 101, Bed A...", self.browser.contents)
1302        self.assertMatches(bed1.owner, self.student_id)
1303        self.assertMatches(bed2.owner, NOT_OCCUPIED)
1304        # Managers can't relocate students if bed is reserved
1305        self.student.sex = u'm'
1306        bed1.bed_type = u'regular_female_reserved'
1307        notify(grok.ObjectModifiedEvent(bed1))
1308        self.browser.getLink("Relocate student").click()
1309        self.assertMatches(
1310            "...Students in reserved beds can't be relocated...",
1311            self.browser.contents)
1312        # Managers can relocate students if booking has been cancelled but
1313        # other bed space has been manually allocated after cancellation
1314        old_owner = bed1.releaseBed()
1315        self.assertMatches(old_owner, self.student_id)
1316        bed2.owner = self.student_id
1317        self.browser.open(self.acco_path + '/2004')
1318        self.assertMatches(
1319            "...booking cancelled...", self.browser.contents)
1320        self.browser.getLink("Relocate student").click()
1321        # We didn't informed the catalog therefore the new owner is not found
1322        self.assertMatches(
1323            "...There is no free bed in your category regular_male_fr...",
1324            self.browser.contents)
1325        # Now we fire the event properly
1326        notify(grok.ObjectModifiedEvent(bed2))
1327        self.browser.getLink("Relocate student").click()
1328        self.assertMatches(
1329            "...Student relocated...", self.browser.contents)
1330        self.assertMatches(
1331            "... Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
1332          # Managers can delete bed tickets
1333        self.browser.open(self.acco_path)
1334        ctrl = self.browser.getControl(name='val_id')
1335        value = ctrl.options[0]
1336        ctrl.getControl(value=value).selected = True
1337        self.browser.getControl("Remove selected", index=0).click()
1338        self.assertMatches('...Successfully removed...', self.browser.contents)
1339        # The bed has been properly released by the event handler
1340        self.assertMatches(bed1.owner, NOT_OCCUPIED)
1341        self.assertMatches(bed2.owner, NOT_OCCUPIED)
1342        return
1343
1344    def test_manage_workflow(self):
1345        # Managers can pass through the whole workflow
1346        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1347        student = self.app['students'][self.student_id]
1348        self.browser.open(self.trigtrans_path)
1349        self.assertTrue(student.clearance_locked)
1350        self.browser.getControl(name="transition").value = ['admit']
1351        self.browser.getControl("Save").click()
1352        self.assertTrue(student.clearance_locked)
1353        self.browser.getControl(name="transition").value = ['start_clearance']
1354        self.browser.getControl("Save").click()
1355        self.assertFalse(student.clearance_locked)
1356        self.browser.getControl(name="transition").value = ['request_clearance']
1357        self.browser.getControl("Save").click()
1358        self.assertTrue(student.clearance_locked)
1359        self.browser.getControl(name="transition").value = ['clear']
1360        self.browser.getControl("Save").click()
1361        # Managers approve payment, they do not pay
1362        self.assertFalse('pay_first_school_fee' in self.browser.contents)
1363        self.browser.getControl(
1364            name="transition").value = ['approve_first_school_fee']
1365        self.browser.getControl("Save").click()
1366        self.browser.getControl(name="transition").value = ['reset6']
1367        self.browser.getControl("Save").click()
1368        # In state returning the pay_school_fee transition triggers some
1369        # changes of attributes
1370        self.browser.getControl(name="transition").value = ['approve_school_fee']
1371        self.browser.getControl("Save").click()
1372        self.assertEqual(student['studycourse'].current_session, 2005) # +1
1373        self.assertEqual(student['studycourse'].current_level, 200) # +100
1374        self.assertEqual(student['studycourse'].current_verdict, '0') # 0 = Zero = not set
1375        self.assertEqual(student['studycourse'].previous_verdict, 'A')
1376        self.browser.getControl(name="transition").value = ['register_courses']
1377        self.browser.getControl("Save").click()
1378        self.browser.getControl(name="transition").value = ['validate_courses']
1379        self.browser.getControl("Save").click()
1380        self.browser.getControl(name="transition").value = ['return']
1381        self.browser.getControl("Save").click()
1382        return
1383
1384    def test_manage_pg_workflow(self):
1385        # Managers can pass through the whole workflow
1386        IWorkflowState(self.student).setState('school fee paid')
1387        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1388        student = self.app['students'][self.student_id]
1389        self.browser.open(self.trigtrans_path)
1390        self.assertTrue('<option value="reset6">' in self.browser.contents)
1391        self.assertTrue('<option value="register_courses">' in self.browser.contents)
1392        self.assertTrue('<option value="reset5">' in self.browser.contents)
1393        self.certificate.study_mode = 'pg_ft'
1394        self.browser.open(self.trigtrans_path)
1395        self.assertFalse('<option value="reset6">' in self.browser.contents)
1396        self.assertFalse('<option value="register_courses">' in self.browser.contents)
1397        self.assertTrue('<option value="reset5">' in self.browser.contents)
1398        return
1399
1400    def test_manage_import(self):
1401        # Managers can import student data files
1402        datacenter_path = 'http://localhost/app/datacenter'
1403        # Prepare a csv file for students
1404        open('students.csv', 'wb').write(
1405"""firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,password
1406Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234,m,mypwd1
1407Claus,Finau,2,1990-01-03,100001,aa@aa.ng,1234,m,mypwd1
1408Brit,Berson,3,1990-01-04,100001,aa@aa.ng,1234,m,mypwd1
1409""")
1410        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1411        self.browser.open(datacenter_path)
1412        self.browser.getLink('Upload data').click()
1413        filecontents = StringIO(open('students.csv', 'rb').read())
1414        filewidget = self.browser.getControl(name='uploadfile:file')
1415        filewidget.add_file(filecontents, 'text/plain', 'students.csv')
1416        self.browser.getControl(name='SUBMIT').click()
1417        self.browser.getLink('Process data').click()
1418        self.browser.getLink("Switch maintenance mode").click()
1419        button = lookup_submit_value(
1420            'select', 'students_zope.mgr.csv', self.browser)
1421        button.click()
1422        importerselect = self.browser.getControl(name='importer')
1423        modeselect = self.browser.getControl(name='mode')
1424        importerselect.getControl('Student Processor').selected = True
1425        modeselect.getControl(value='create').selected = True
1426        self.browser.getControl('Proceed to step 3').click()
1427        self.assertTrue('Header fields OK' in self.browser.contents)
1428        self.browser.getControl('Perform import').click()
1429        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
1430        self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
1431        self.assertTrue('Batch processing finished' in self.browser.contents)
1432        open('studycourses.csv', 'wb').write(
1433"""reg_number,matric_number,certificate,current_session,current_level
14341,,CERT1,2008,100
1435,100001,CERT1,2008,100
1436,100002,CERT1,2008,100
1437""")
1438        self.browser.open(datacenter_path)
1439        self.browser.getLink('Upload data').click()
1440        filecontents = StringIO(open('studycourses.csv', 'rb').read())
1441        filewidget = self.browser.getControl(name='uploadfile:file')
1442        filewidget.add_file(filecontents, 'text/plain', 'studycourses.csv')
1443        self.browser.getControl(name='SUBMIT').click()
1444        self.browser.getLink('Process data').click()
1445        # Meanwhile maintenance mode is disabled again.
1446        self.browser.getLink("Switch maintenance mode").click()
1447        button = lookup_submit_value(
1448            'select', 'studycourses_zope.mgr.csv', self.browser)
1449        button.click()
1450        importerselect = self.browser.getControl(name='importer')
1451        modeselect = self.browser.getControl(name='mode')
1452        importerselect.getControl(
1453            'StudentStudyCourse Processor (update only)').selected = True
1454        modeselect.getControl(value='create').selected = True
1455        self.browser.getControl('Proceed to step 3').click()
1456        self.assertTrue('Update mode only' in self.browser.contents)
1457        self.browser.getControl('Proceed to step 3').click()
1458        self.assertTrue('Header fields OK' in self.browser.contents)
1459        self.browser.getControl('Perform import').click()
1460        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
1461        self.assertTrue('Successfully processed 2 rows'
1462                        in self.browser.contents)
1463        # The students are properly indexed and we can
1464        # thus find a student in  the department
1465        self.browser.open(self.manage_container_path)
1466        self.browser.getControl(name="searchtype").value = ['depcode']
1467        self.browser.getControl(name="searchterm").value = 'dep1'
1468        self.browser.getControl("Find student(s)").click()
1469        self.assertTrue('Aaren Pieri' in self.browser.contents)
1470        # We can search for a new student by name ...
1471        self.browser.getControl(name="searchtype").value = ['fullname']
1472        self.browser.getControl(name="searchterm").value = 'Claus'
1473        self.browser.getControl("Find student(s)").click()
1474        self.assertTrue('Claus Finau' in self.browser.contents)
1475        # ... and check if the imported password has been properly set
1476        ctrl = self.browser.getControl(name='entries')
1477        value = ctrl.options[0]
1478        claus = self.app['students'][value]
1479        self.assertTrue(IUserAccount(claus).checkPassword('mypwd1'))
1480        return
1481
1482    def init_clearance_officer(self):
1483        # Create clearance officer
1484        self.app['users'].addUser('mrclear', 'mrclearsecret')
1485        self.app['users']['mrclear'].email = 'mrclear@foo.ng'
1486        self.app['users']['mrclear'].title = 'Carlo Pitter'
1487        # Clearance officers need not necessarily to get
1488        # the StudentsOfficer site role
1489        #prmglobal = IPrincipalRoleManager(self.app)
1490        #prmglobal.assignRoleToPrincipal('waeup.StudentsOfficer', 'mrclear')
1491        # Assign local ClearanceOfficer role
1492        self.department = self.app['faculties']['fac1']['dep1']
1493        prmlocal = IPrincipalRoleManager(self.department)
1494        prmlocal.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'mrclear')
1495        IWorkflowState(self.student).setState('clearance started')
1496        # Add another student for testing
1497        other_student = Student()
1498        other_student.firstname = u'Dep2'
1499        other_student.lastname = u'Student'
1500        self.app['students'].addStudent(other_student)
1501        self.other_student_path = (
1502            'http://localhost/app/students/%s' % other_student.student_id)
1503        # Login as clearance officer
1504        self.browser.open(self.login_path)
1505        self.browser.getControl(name="form.login").value = 'mrclear'
1506        self.browser.getControl(name="form.password").value = 'mrclearsecret'
1507        self.browser.getControl("Login").click()
1508
1509    def test_handle_clearance_by_co(self):
1510        self.init_clearance_officer()
1511        self.assertMatches('...You logged in...', self.browser.contents)
1512        # CO is landing on index page.
1513        self.assertEqual(self.browser.url, 'http://localhost/app/index')
1514        # CO can see his roles
1515        self.browser.getLink("My Roles").click()
1516        self.assertMatches(
1517            '...<div>Academics Officer (view only)</div>...',
1518            self.browser.contents)
1519        # But not his local role ...
1520        self.assertFalse('Clearance Officer' in self.browser.contents)
1521        # ... because we forgot to notify the department that the local role
1522        # has changed.
1523        notify(LocalRoleSetEvent(
1524            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
1525            granted=True))
1526        self.browser.open('http://localhost/app/users/mrclear/my_roles')
1527        self.assertTrue('Clearance Officer' in self.browser.contents)
1528        self.assertMatches(
1529            '...<a href="http://localhost/app/faculties/fac1/dep1">...',
1530            self.browser.contents)
1531        # CO can view the student ...
1532        self.browser.open(self.clearance_path)
1533        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1534        self.assertEqual(self.browser.url, self.clearance_path)
1535        # ... but not other students.
1536        self.assertRaises(
1537            Unauthorized, self.browser.open, self.other_student_path)
1538        # Clearance is disabled for this session.
1539        self.browser.open(self.clearance_path)
1540        self.assertFalse('Clear student' in self.browser.contents)
1541        self.browser.open(self.student_path + '/clear')
1542        self.assertTrue('Clearance is disabled for this session'
1543            in self.browser.contents)
1544        self.app['configuration']['2004'].clearance_enabled = True
1545        # Only in state clearance requested the CO does see the 'Clear' button.
1546        self.browser.open(self.clearance_path)
1547        self.assertFalse('Clear student' in self.browser.contents)
1548        self.browser.open(self.student_path + '/clear')
1549        self.assertTrue('Student is in wrong state.'
1550            in self.browser.contents)
1551        IWorkflowInfo(self.student).fireTransition('request_clearance')
1552        self.browser.open(self.clearance_path)
1553        self.assertTrue('Clear student' in self.browser.contents)
1554        self.browser.getLink("Clear student").click()
1555        self.assertTrue('Student has been cleared' in self.browser.contents)
1556        self.assertTrue('cleared' in self.browser.contents)
1557        self.browser.open(self.history_path)
1558        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
1559        # Hide real name.
1560        self.app['users']['mrclear'].public_name = 'My Public Name'
1561        self.browser.open(self.clearance_path)
1562        self.browser.getLink("Reject clearance").click()
1563        self.assertEqual(
1564            self.browser.url, self.student_path + '/reject_clearance')
1565        # Type comment why.
1566        self.browser.getControl(name="form.officer_comment").value = (
1567            'Dear Student,\n'
1568            'You did not fill properly.')
1569        self.browser.getControl("Save comment").click()
1570        self.assertTrue('Clearance has been annulled' in self.browser.contents)
1571        url = ('http://localhost/app/students/K1000000/'
1572              'contactstudent?body=Dear+Student%2C%0AYou+did+not+fill+properly.'
1573              '&subject=Clearance+has+been+annulled.')
1574        # CO does now see the prefilled contact form and can send a message.
1575        self.assertEqual(self.browser.url, url)
1576        self.assertTrue('clearance started' in self.browser.contents)
1577        self.assertTrue('name="form.subject" size="20" type="text" '
1578            'value="Clearance has been annulled."'
1579            in self.browser.contents)
1580        self.assertTrue('name="form.body" rows="10" >Dear Student,'
1581            in self.browser.contents)
1582        self.browser.getControl("Send message now").click()
1583        self.assertTrue('Your message has been sent' in self.browser.contents)
1584        # The comment has been stored ...
1585        self.assertEqual(self.student.officer_comment,
1586            u'Dear Student,\nYou did not fill properly.')
1587        # ... and logged.
1588        logfile = os.path.join(
1589            self.app['datacenter'].storage, 'logs', 'students.log')
1590        logcontent = open(logfile).read()
1591        self.assertTrue(
1592            'INFO - mrclear - students.browser.StudentRejectClearancePage - '
1593            'K1000000 - comment: Dear Student,<br>You did not fill '
1594            'properly.\n' in logcontent)
1595        self.browser.open(self.history_path)
1596        self.assertTrue("Reset to 'clearance started' by My Public Name" in
1597            self.browser.contents)
1598        IWorkflowInfo(self.student).fireTransition('request_clearance')
1599        self.browser.open(self.clearance_path)
1600        self.browser.getLink("Reject clearance").click()
1601        self.browser.getControl("Save comment").click()
1602        self.assertTrue('Clearance request has been rejected'
1603            in self.browser.contents)
1604        self.assertTrue('clearance started' in self.browser.contents)
1605        # The CO can't clear students if not in state
1606        # clearance requested.
1607        self.browser.open(self.student_path + '/clear')
1608        self.assertTrue('Student is in wrong state'
1609            in self.browser.contents)
1610        # The CO can go to his department throug the my_roles page ...
1611        self.browser.open('http://localhost/app/users/mrclear/my_roles')
1612        self.browser.getLink("http://localhost/app/faculties/fac1/dep1").click()
1613        # ... and view the list of students.
1614        self.browser.getLink("Show students").click()
1615        self.browser.getControl(name="session").value = ['2004']
1616        self.browser.getControl(name="level").value = ['200']
1617        self.browser.getControl("Show").click()
1618        self.assertFalse(self.student_id in self.browser.contents)
1619        self.browser.getControl(name="session").value = ['2004']
1620        self.browser.getControl(name="level").value = ['100']
1621        self.browser.getControl("Show").click()
1622        self.assertTrue(self.student_id in self.browser.contents)
1623        # The comment is indicated by 'yes'.
1624        self.assertTrue('<td><span>yes</span></td>' in self.browser.contents)
1625        # Check if the enquiries form is not pre-filled with officer_comment
1626        # (regression test).
1627        self.browser.getLink("Logout").click()
1628        self.browser.open('http://localhost/app/enquiries')
1629        self.assertFalse(
1630            'You did not fill properly'
1631            in self.browser.contents)
1632        # When a student is cleared the comment is automatically deleted
1633        IWorkflowInfo(self.student).fireTransition('request_clearance')
1634        IWorkflowInfo(self.student).fireTransition('clear')
1635        self.assertEqual(self.student.officer_comment, None)
1636        return
1637
1638    def test_handle_mass_clearance_by_co(self):
1639        self.init_clearance_officer()
1640        # Additional setups according to test above
1641        notify(LocalRoleSetEvent(
1642            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
1643            granted=True))
1644        self.app['configuration']['2004'].clearance_enabled = True
1645        IWorkflowState(self.student).setState('clearance requested')
1646        # Update the catalog
1647        notify(grok.ObjectModifiedEvent(self.student))
1648        # The CO can go to the department and clear all students in department
1649        self.browser.open('http://localhost/app/faculties/fac1/dep1')
1650        self.browser.getLink("Clear all students").click()
1651        self.assertTrue('1 students have been cleared' in self.browser.contents)
1652        self.browser.open(self.history_path)
1653        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
1654        logfile = os.path.join(
1655            self.app['datacenter'].storage, 'logs', 'students.log')
1656        logcontent = open(logfile).read()
1657        self.assertTrue(
1658            'INFO - mrclear - K1000000 - Cleared' in logcontent)
1659        self.browser.open('http://localhost/app/faculties/fac1/dep1')
1660        self.browser.getLink("Clear all students").click()
1661        self.assertTrue('0 students have been cleared' in self.browser.contents)
1662        return
1663
1664    def test_handle_courses_by_ca(self):
1665        self.app['users'].addUser('mrsadvise', 'mrsadvisesecret')
1666        self.app['users']['mrsadvise'].email = 'mradvise@foo.ng'
1667        self.app['users']['mrsadvise'].title = u'Helen Procter'
1668        # Assign local CourseAdviser100 role for a certificate
1669        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
1670        prmlocal = IPrincipalRoleManager(cert)
1671        prmlocal.assignRoleToPrincipal('waeup.local.CourseAdviser100', 'mrsadvise')
1672        IWorkflowState(self.student).setState('school fee paid')
1673        # Login as course adviser.
1674        self.browser.open(self.login_path)
1675        self.browser.getControl(name="form.login").value = 'mrsadvise'
1676        self.browser.getControl(name="form.password").value = 'mrsadvisesecret'
1677        self.browser.getControl("Login").click()
1678        self.assertMatches('...You logged in...', self.browser.contents)
1679        # CO can see his roles.
1680        self.browser.getLink("My Roles").click()
1681        self.assertMatches(
1682            '...<div>Academics Officer (view only)</div>...',
1683            self.browser.contents)
1684        # But not his local role ...
1685        self.assertFalse('Course Adviser' in self.browser.contents)
1686        # ... because we forgot to notify the certificate that the local role
1687        # has changed.
1688        notify(LocalRoleSetEvent(
1689            cert, 'waeup.local.CourseAdviser100', 'mrsadvise', granted=True))
1690        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
1691        self.assertTrue('Course Adviser 100L' in self.browser.contents)
1692        self.assertMatches(
1693            '...<a href="http://localhost/app/faculties/fac1/dep1/certificates/CERT1">...',
1694            self.browser.contents)
1695        # CA can view the student ...
1696        self.browser.open(self.student_path)
1697        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1698        self.assertEqual(self.browser.url, self.student_path)
1699        # ... but not other students.
1700        other_student = Student()
1701        other_student.firstname = u'Dep2'
1702        other_student.lastname = u'Student'
1703        self.app['students'].addStudent(other_student)
1704        other_student_path = (
1705            'http://localhost/app/students/%s' % other_student.student_id)
1706        self.assertRaises(
1707            Unauthorized, self.browser.open, other_student_path)
1708        # We add study level 110 to the student's studycourse.
1709        studylevel = StudentStudyLevel()
1710        studylevel.level = 110
1711        self.student['studycourse'].addStudentStudyLevel(
1712            cert,studylevel)
1713        L110_student_path = self.studycourse_path + '/110'
1714        # The CA can neither see the Validate nor the Edit button.
1715        self.browser.open(L110_student_path)
1716        self.assertFalse('Validate courses' in self.browser.contents)
1717        self.assertFalse('Edit' in self.browser.contents)
1718        IWorkflowInfo(self.student).fireTransition('register_courses')
1719        self.browser.open(L110_student_path)
1720        self.assertFalse('Validate courses' in self.browser.contents)
1721        self.assertFalse('Edit' in self.browser.contents)
1722        # Only in state courses registered and only if the current level
1723        # corresponds with the name of the study level object
1724        # the 100L CA does see the 'Validate' button but not
1725        # the edit button.
1726        self.student['studycourse'].current_level = 110
1727        self.browser.open(L110_student_path)
1728        self.assertFalse('Edit' in self.browser.contents)
1729        self.assertTrue('Validate courses' in self.browser.contents)
1730        # But a 100L CA does not see the button at other levels.
1731        studylevel2 = StudentStudyLevel()
1732        studylevel2.level = 200
1733        self.student['studycourse'].addStudentStudyLevel(
1734            cert,studylevel2)
1735        L200_student_path = self.studycourse_path + '/200'
1736        self.browser.open(L200_student_path)
1737        self.assertFalse('Edit' in self.browser.contents)
1738        self.assertFalse('Validate courses' in self.browser.contents)
1739        self.browser.open(L110_student_path)
1740        self.browser.getLink("Validate courses").click()
1741        self.assertTrue('Course list has been validated' in self.browser.contents)
1742        self.assertTrue('courses validated' in self.browser.contents)
1743        self.assertEqual(self.student['studycourse']['110'].validated_by,
1744            'Helen Procter')
1745        self.assertMatches(
1746            '<YYYY-MM-DD hh:mm:ss>',
1747            self.student['studycourse']['110'].validation_date.strftime(
1748                "%Y-%m-%d %H:%M:%S"))
1749        self.browser.getLink("Reject courses").click()
1750        self.assertTrue('Course list request has been annulled.'
1751            in self.browser.contents)
1752        urlmessage = 'Course+list+request+has+been+annulled.'
1753        self.assertEqual(self.browser.url, self.student_path +
1754            '/contactstudent?subject=%s' % urlmessage)
1755        self.assertTrue('school fee paid' in self.browser.contents)
1756        self.assertTrue(self.student['studycourse']['110'].validated_by is None)
1757        self.assertTrue(self.student['studycourse']['110'].validation_date is None)
1758        IWorkflowInfo(self.student).fireTransition('register_courses')
1759        self.browser.open(L110_student_path)
1760        self.browser.getLink("Reject courses").click()
1761        self.assertTrue('Course list has been unregistered'
1762            in self.browser.contents)
1763        self.assertTrue('school fee paid' in self.browser.contents)
1764        # CA does now see the contact form and can send a message.
1765        self.browser.getControl(name="form.subject").value = 'Important subject'
1766        self.browser.getControl(name="form.body").value = 'Course list rejected'
1767        self.browser.getControl("Send message now").click()
1768        self.assertTrue('Your message has been sent' in self.browser.contents)
1769        # The CA does now see the Edit button and can edit
1770        # current study level.
1771        self.browser.open(L110_student_path)
1772        self.browser.getLink("Edit").click()
1773        self.assertTrue('Edit course list of 100 (Year 1) on 1st probation'
1774            in self.browser.contents)
1775        # The CA can't validate courses if not in state
1776        # courses registered.
1777        self.browser.open(L110_student_path + '/validate_courses')
1778        self.assertTrue('Student is in the wrong state'
1779            in self.browser.contents)
1780        # The CA can go to his certificate through the my_roles page ...
1781        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
1782        self.browser.getLink(
1783            "http://localhost/app/faculties/fac1/dep1/certificates/CERT1").click()
1784        # ... and view the list of students.
1785        self.browser.getLink("Show students").click()
1786        self.browser.getControl(name="session").value = ['2004']
1787        self.browser.getControl(name="level").value = ['100']
1788        self.browser.getControl("Show").click()
1789        self.assertTrue(self.student_id in self.browser.contents)
1790
1791    def test_handle_courses_by_lecturer(self):
1792        self.app['users'].addUser('mrslecturer', 'mrslecturersecret')
1793        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
1794        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
1795        # Assign local Lecturer role for a certificate.
1796        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
1797        prmlocal = IPrincipalRoleManager(course)
1798        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
1799        # Login as lecturer.
1800        self.browser.open(self.login_path)
1801        self.browser.getControl(name="form.login").value = 'mrslecturer'
1802        self.browser.getControl(name="form.password").value = 'mrslecturersecret'
1803        self.browser.getControl("Login").click()
1804        self.assertMatches('...You logged in...', self.browser.contents)
1805        # CO can see her roles.
1806        self.browser.getLink("My Roles").click()
1807        self.assertMatches(
1808            '...<div>Academics Officer (view only)</div>...',
1809            self.browser.contents)
1810        # But not her local role ...
1811        self.assertFalse('Lecturer' in self.browser.contents)
1812        # ... because we forgot to notify the course that the local role
1813        # has changed.
1814        notify(LocalRoleSetEvent(
1815            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
1816        self.browser.open('http://localhost/app/users/mrslecturer/my_roles')
1817        self.assertTrue('Lecturer' in self.browser.contents)
1818        self.assertMatches(
1819            '...<a href="http://localhost/app/faculties/fac1/dep1/courses/COURSE1">...',
1820            self.browser.contents)
1821        # The lecturer can go to her course ...
1822        self.browser.getLink(
1823            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1").click()
1824        # ... and view the list of students
1825        self.browser.getLink("Show students").click()
1826        self.browser.getControl(name="session").value = ['2004']
1827        self.browser.getControl(name="level").value = ['100']
1828        self.browser.getControl("Show").click()
1829        self.assertTrue('No student found.' in self.browser.contents)
1830        # No student in course so far.
1831        self.assertFalse(self.student_id in self.browser.contents)
1832        studylevel = createObject(u'waeup.StudentStudyLevel')
1833        studylevel.level = 100
1834        studylevel.level_session = 2004
1835        self.student['studycourse'].addStudentStudyLevel(
1836            self.certificate, studylevel)
1837        # Now the student has registered the course and can
1838        # be seen by the lecturer.
1839        self.browser.open("http://localhost/app/faculties/fac1/dep1/courses/COURSE1/students")
1840        self.browser.getControl(name="session").value = ['2004']
1841        self.browser.getControl(name="level").value = ['100']
1842        self.browser.getControl("Show").click()
1843        self.assertTrue(self.student_id in self.browser.contents)
1844        # Lecturer can neither access the student ...
1845        self.assertRaises(
1846            Unauthorized, self.browser.open, self.student_path)
1847        # ... nor the respective course ticket since
1848        # editing course tickets by lecturers is not feasible.
1849        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
1850        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
1851        self.assertRaises(
1852            Unauthorized, self.browser.open, course_ticket_path)
1853        # Course results can be batch edited via the edit_courses view.
1854        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
1855        self.browser.open("http://localhost/app/faculties/fac1/dep1/courses/COURSE1")
1856        self.browser.getLink("Update scores").click()
1857        self.assertTrue('Score editing disabled' in self.browser.contents)
1858        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
1859        self.browser.getLink("Update scores").click()
1860        self.assertTrue('Current academic session not set' in self.browser.contents)
1861        self.app['configuration'].current_academic_session = 2004
1862        self.browser.getLink("Update scores").click()
1863        self.assertFalse(
1864            '<input type="text" name="scores" class="span1" />'
1865            in self.browser.contents)
1866        IWorkflowState(self.student).setState('courses validated')
1867        # Student must be in state 'courses validated'.
1868        self.browser.open(
1869            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1870        self.assertTrue(
1871            '<input type="text" name="scores" class="span1" />'
1872            in self.browser.contents)
1873        self.browser.getControl(name="scores", index=0).value = '55'
1874        self.browser.getControl("Update scores").click()
1875        # New score has been set.
1876        self.assertEqual(
1877            self.student['studycourse']['100']['COURSE1'].score, 55)
1878        # Score editing has been logged.
1879        logfile = os.path.join(
1880            self.app['datacenter'].storage, 'logs', 'students.log')
1881        logcontent = open(logfile).read()
1882        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
1883                        'K1000000 100/COURSE1 score updated (55)' in logcontent)
1884        # Non-integer scores won't be accepted.
1885        self.browser.open(
1886            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1887        self.assertTrue('value="55" />' in self.browser.contents)
1888        self.browser.getControl(name="scores", index=0).value = 'abc'
1889        self.browser.getControl("Update scores").click()
1890        self.assertTrue('Error: Score(s) of Anna Tester have not be updated'
1891            in self.browser.contents)
1892        # Scores can be removed.
1893        self.browser.open(
1894            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1895        self.browser.getControl(name="scores", index=0).value = ''
1896        self.browser.getControl("Update scores").click()
1897        self.assertEqual(
1898            self.student['studycourse']['100']['COURSE1'].score, None)
1899        logcontent = open(logfile).read()
1900        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
1901                        'K1000000 100/COURSE1 score updated (None)' in logcontent)
1902
1903    def test_change_current_mode(self):
1904        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1905        self.browser.open(self.clearance_path)
1906        self.assertFalse('Employer' in self.browser.contents)
1907        self.browser.open(self.manage_clearance_path)
1908        self.assertFalse('Employer' in self.browser.contents)
1909        self.browser.open(self.edit_clearance_path)
1910        self.assertFalse('Employer' in self.browser.contents)
1911        # Now we change the study mode of the certificate and a different
1912        # interface is used by clearance views.
1913        self.certificate.study_mode = 'pg_ft'
1914        # Invariants are not being checked here?!
1915        self.certificate.end_level = 100
1916        self.browser.open(self.clearance_path)
1917        self.assertTrue('Employer' in self.browser.contents)
1918        self.browser.open(self.manage_clearance_path)
1919        self.assertTrue('Employer' in self.browser.contents)
1920        IWorkflowState(self.student).setState('clearance started')
1921        self.browser.open(self.edit_clearance_path)
1922        self.assertTrue('Employer' in self.browser.contents)
1923
1924    def test_find_students_in_faculties(self):
1925        # Create local students manager in faculty
1926        self.app['users'].addUser('mrmanager', 'mrmanagersecret')
1927        self.app['users']['mrmanager'].email = 'mrmanager@foo.ng'
1928        self.app['users']['mrmanager'].title = u'Volk Wagen'
1929        # Assign LocalStudentsManager role for faculty
1930        fac = self.app['faculties']['fac1']
1931        prmlocal = IPrincipalRoleManager(fac)
1932        prmlocal.assignRoleToPrincipal(
1933            'waeup.local.LocalStudentsManager', 'mrmanager')
1934        notify(LocalRoleSetEvent(
1935            fac, 'waeup.local.LocalStudentsManager', 'mrmanager',
1936            granted=True))
1937        # Login as manager
1938        self.browser.open(self.login_path)
1939        self.browser.getControl(name="form.login").value = 'mrmanager'
1940        self.browser.getControl(name="form.password").value = 'mrmanagersecret'
1941        self.browser.getControl("Login").click()
1942        self.assertMatches('...You logged in...', self.browser.contents)
1943        # Manager can see his roles
1944        self.browser.getLink("My Roles").click()
1945        self.assertMatches(
1946            '...<span>Students Manager</span>...',
1947            self.browser.contents)
1948        # The manager can go to his faculty
1949        self.browser.getLink(
1950            "http://localhost/app/faculties/fac1").click()
1951        # and find students
1952        self.browser.getLink("Find students").click()
1953        self.browser.getControl("Find student").click()
1954        self.assertTrue('Empty search string' in self.browser.contents)
1955        self.browser.getControl(name="searchtype").value = ['student_id']
1956        self.browser.getControl(name="searchterm").value = self.student_id
1957        self.browser.getControl("Find student").click()
1958        self.assertTrue('Anna Tester' in self.browser.contents)
1959
1960    def test_activate_deactivate_buttons(self):
1961        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1962        self.browser.open(self.student_path)
1963        self.browser.getLink("Deactivate").click()
1964        self.assertTrue(
1965            'Student account has been deactivated.' in self.browser.contents)
1966        self.assertTrue(
1967            'Base Data (account deactivated)' in self.browser.contents)
1968        self.assertTrue(self.student.suspended)
1969        self.browser.getLink("Activate").click()
1970        self.assertTrue(
1971            'Student account has been activated.' in self.browser.contents)
1972        self.assertFalse(
1973            'Base Data (account deactivated)' in self.browser.contents)
1974        self.assertFalse(self.student.suspended)
1975        # History messages have been added ...
1976        self.browser.getLink("History").click()
1977        self.assertTrue(
1978            'Student account deactivated by Manager<br />' in self.browser.contents)
1979        self.assertTrue(
1980            'Student account activated by Manager<br />' in self.browser.contents)
1981        # ... and actions have been logged.
1982        logfile = os.path.join(
1983            self.app['datacenter'].storage, 'logs', 'students.log')
1984        logcontent = open(logfile).read()
1985        self.assertTrue('zope.mgr - students.browser.StudentDeactivateView - '
1986                        'K1000000 - account deactivated' in logcontent)
1987        self.assertTrue('zope.mgr - students.browser.StudentActivateView - '
1988                        'K1000000 - account activated' in logcontent)
1989
1990    def test_manage_student_transfer(self):
1991        # Add second certificate
1992        self.certificate2 = createObject('waeup.Certificate')
1993        self.certificate2.code = u'CERT2'
1994        self.certificate2.study_mode = 'ug_ft'
1995        self.certificate2.start_level = 999
1996        self.certificate2.end_level = 999
1997        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
1998            self.certificate2)
1999
2000        # Add study level to old study course
2001        studylevel = createObject(u'waeup.StudentStudyLevel')
2002        studylevel.level = 200
2003        self.student['studycourse'].addStudentStudyLevel(
2004            self.certificate, studylevel)
2005        studylevel = createObject(u'waeup.StudentStudyLevel')
2006        studylevel.level = 999
2007        self.student['studycourse'].addStudentStudyLevel(
2008            self.certificate, studylevel)
2009
2010        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
2011        self.browser.open(self.student_path)
2012        self.browser.getLink("Transfer").click()
2013        self.browser.getControl(name="form.certificate").value = ['CERT2']
2014        self.browser.getControl(name="form.current_session").value = ['2011']
2015        self.browser.getControl(name="form.current_level").value = ['200']
2016        self.browser.getControl("Transfer").click()
2017        self.assertTrue(
2018            'Current level does not match certificate levels'
2019            in self.browser.contents)
2020        self.browser.getControl(name="form.current_level").value = ['999']
2021        self.browser.getControl("Transfer").click()
2022        self.assertTrue('Successfully transferred' in self.browser.contents)
2023        # The catalog has been updated
2024        cat = queryUtility(ICatalog, name='students_catalog')
2025        results = list(
2026            cat.searchResults(
2027            certcode=('CERT2', 'CERT2')))
2028        self.assertTrue(results[0] is self.student)
2029        results = list(
2030            cat.searchResults(
2031            current_session=(2011, 2011)))
2032        self.assertTrue(results[0] is self.student)
2033        # Add study level to new study course
2034        studylevel = createObject(u'waeup.StudentStudyLevel')
2035        studylevel.level = 999
2036        self.student['studycourse'].addStudentStudyLevel(
2037            self.certificate, studylevel)
2038
2039        # Edit and add pages are locked for old study courses
2040        self.browser.open(self.student_path + '/studycourse/manage')
2041        self.assertFalse('The requested form is locked' in self.browser.contents)
2042        self.browser.open(self.student_path + '/studycourse_1/manage')
2043        self.assertTrue('The requested form is locked' in self.browser.contents)
2044
2045        self.browser.open(self.student_path + '/studycourse/start_session')
2046        self.assertFalse('The requested form is locked' in self.browser.contents)
2047        self.browser.open(self.student_path + '/studycourse_1/start_session')
2048        self.assertTrue('The requested form is locked' in self.browser.contents)
2049
2050        IWorkflowState(self.student).setState('school fee paid')
2051        self.browser.open(self.student_path + '/studycourse/add')
2052        self.assertFalse('The requested form is locked' in self.browser.contents)
2053        self.browser.open(self.student_path + '/studycourse_1/add')
2054        self.assertTrue('The requested form is locked' in self.browser.contents)
2055
2056        self.browser.open(self.student_path + '/studycourse/999/manage')
2057        self.assertFalse('The requested form is locked' in self.browser.contents)
2058        self.browser.open(self.student_path + '/studycourse_1/999/manage')
2059        self.assertTrue('The requested form is locked' in self.browser.contents)
2060
2061        self.browser.open(self.student_path + '/studycourse/999/validate_courses')
2062        self.assertFalse('The requested form is locked' in self.browser.contents)
2063        self.browser.open(self.student_path + '/studycourse_1/999/validate_courses')
2064        self.assertTrue('The requested form is locked' in self.browser.contents)
2065
2066        self.browser.open(self.student_path + '/studycourse/999/reject_courses')
2067        self.assertFalse('The requested form is locked' in self.browser.contents)
2068        self.browser.open(self.student_path + '/studycourse_1/999/reject_courses')
2069        self.assertTrue('The requested form is locked' in self.browser.contents)
2070
2071        self.browser.open(self.student_path + '/studycourse/999/add')
2072        self.assertFalse('The requested form is locked' in self.browser.contents)
2073        self.browser.open(self.student_path + '/studycourse_1/999/add')
2074        self.assertTrue('The requested form is locked' in self.browser.contents)
2075
2076        self.browser.open(self.student_path + '/studycourse/999/edit')
2077        self.assertFalse('The requested form is locked' in self.browser.contents)
2078        self.browser.open(self.student_path + '/studycourse_1/999/edit')
2079        self.assertTrue('The requested form is locked' in self.browser.contents)
2080
2081        # Revert transfer
2082        self.browser.open(self.student_path + '/studycourse_1')
2083        self.browser.getLink("Reactivate").click()
2084        self.browser.getControl("Revert now").click()
2085        self.assertTrue('Previous transfer reverted' in self.browser.contents)
2086        results = list(
2087            cat.searchResults(
2088            certcode=('CERT1', 'CERT1')))
2089        self.assertTrue(results[0] is self.student)
2090        self.assertEqual([i for i in self.student.keys()],
2091            [u'accommodation', u'payments', u'studycourse'])
2092
2093    def test_login_as_student(self):
2094        # StudentImpersonators can login as student
2095        # Create clearance officer
2096        self.app['users'].addUser('mrofficer', 'mrofficersecret')
2097        self.app['users']['mrofficer'].email = 'mrofficer@foo.ng'
2098        self.app['users']['mrofficer'].title = 'Harry Actor'
2099        prmglobal = IPrincipalRoleManager(self.app)
2100        prmglobal.assignRoleToPrincipal('waeup.StudentImpersonator', 'mrofficer')
2101        prmglobal.assignRoleToPrincipal('waeup.StudentsManager', 'mrofficer')
2102        # Login as student impersonator
2103        self.browser.open(self.login_path)
2104        self.browser.getControl(name="form.login").value = 'mrofficer'
2105        self.browser.getControl(name="form.password").value = 'mrofficersecret'
2106        self.browser.getControl("Login").click()
2107        self.assertMatches('...You logged in...', self.browser.contents)
2108        self.browser.open(self.student_path)
2109        self.browser.getLink("Login as").click()
2110        self.browser.getControl("Set password now").click()
2111        temp_password = self.browser.getControl(name='form.password').value
2112        self.browser.getControl("Login now").click()
2113        self.assertMatches(
2114            '...You successfully logged in as...', self.browser.contents)
2115        # We are logged in as student and can see the 'My Data' tab
2116        self.assertMatches(
2117            '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
2118            self.browser.contents)
2119        self.assertMatches(
2120            '...My Data...',
2121            self.browser.contents)
2122        self.browser.getLink("Logout").click()
2123        # The student can't login with the original password ...
2124        self.browser.open(self.login_path)
2125        self.browser.getControl(name="form.login").value = self.student_id
2126        self.browser.getControl(name="form.password").value = 'spwd'
2127        self.browser.getControl("Login").click()
2128        self.assertMatches(
2129            '...Your account has been temporarily deactivated...',
2130            self.browser.contents)
2131        # ... but with the temporary password
2132        self.browser.open(self.login_path)
2133        self.browser.getControl(name="form.login").value = self.student_id
2134        self.browser.getControl(name="form.password").value = temp_password
2135        self.browser.getControl("Login").click()
2136        self.assertMatches('...You logged in...', self.browser.contents)
2137        # Creation of temp_password is properly logged
2138        logfile = os.path.join(
2139            self.app['datacenter'].storage, 'logs', 'students.log')
2140        logcontent = open(logfile).read()
2141        self.assertTrue(
2142            'mrofficer - students.browser.LoginAsStudentStep1 - K1000000 - '
2143            'temp_password generated: %s' % temp_password in logcontent)
2144
2145    def test_transcripts(self):
2146        studylevel = createObject(u'waeup.StudentStudyLevel')
2147        studylevel.level = 100
2148        studylevel.level_session = 2005
2149        self.student['studycourse'].entry_mode = 'ug_ft'
2150        self.student['studycourse'].addStudentStudyLevel(
2151            self.certificate, studylevel)
2152        studylevel2 = createObject(u'waeup.StudentStudyLevel')
2153        studylevel2.level = 110
2154        studylevel2.level_session = 2006
2155        self.student['studycourse'].addStudentStudyLevel(
2156            self.certificate, studylevel2)
2157        # Add second course (COURSE has been added automatically)
2158        courseticket = createObject('waeup.CourseTicket')
2159        courseticket.code = 'ANYCODE'
2160        courseticket.title = u'Any TITLE'
2161        courseticket.credits = 13
2162        courseticket.score = 66
2163        courseticket.semester = 1
2164        courseticket.dcode = u'ANYDCODE'
2165        courseticket.fcode = u'ANYFCODE'
2166        self.student['studycourse']['110']['COURSE2'] = courseticket
2167        self.student['studycourse']['100']['COURSE1'].score = 55
2168        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 3.0)
2169        self.assertEqual(self.student['studycourse']['110'].gpa_params_rectified[0], 4.0)
2170        # Get transcript data
2171        td = self.student['studycourse'].getTranscriptData()
2172        self.assertEqual(td[0][0]['level_key'], '100')
2173        self.assertEqual(td[0][0]['sgpa'], 3.0)
2174        self.assertEqual(td[0][0]['level'].level, 100)
2175        self.assertEqual(td[0][0]['level'].level_session, 2005)
2176        self.assertEqual(td[0][0]['tickets_1'][0].code, 'COURSE1')
2177        self.assertEqual(td[0][1]['level_key'], '110')
2178        self.assertEqual(td[0][1]['sgpa'], 4.0)
2179        self.assertEqual(td[0][1]['level'].level, 110)
2180        self.assertEqual(td[0][1]['level'].level_session, 2006)
2181        self.assertEqual(td[0][1]['tickets_1'][0].code, 'ANYCODE')
2182        self.assertEqual(td[1], 3.57)
2183        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
2184        self.browser.open(self.student_path + '/studycourse/transcript')
2185        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2186        self.assertTrue('Transcript' in self.browser.contents)
2187        # Officers can open the pdf transcript
2188        self.browser.open(self.student_path + '/studycourse/transcript.pdf')
2189        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2190        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2191        path = os.path.join(samples_dir(), 'transcript.pdf')
2192        open(path, 'wb').write(self.browser.contents)
2193        print "Sample PDF transcript.pdf written to %s" % path
2194
2195    def test_process_transcript_request(self):
2196        IWorkflowState(self.student).setState('transcript requested')
2197        notify(grok.ObjectModifiedEvent(self.student))
2198        self.student.transcript_comment = (
2199            u'On 07/08/2013 08:59:54 UTC K1000000 wrote:\n\nComment line 1 \n'
2200            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
2201            'Address line2\n\n')
2202        # Create transcript officer
2203        self.app['users'].addUser('mrtranscript', 'mrtranscriptsecret')
2204        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
2205        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
2206        prmglobal = IPrincipalRoleManager(self.app)
2207        prmglobal.assignRoleToPrincipal('waeup.TranscriptOfficer', 'mrtranscript')
2208        # Login as transcript officer
2209        self.browser.open(self.login_path)
2210        self.browser.getControl(name="form.login").value = 'mrtranscript'
2211        self.browser.getControl(name="form.password").value = 'mrtranscriptsecret'
2212        self.browser.getControl("Login").click()
2213        self.assertMatches('...You logged in...', self.browser.contents)
2214        # Officer can see his roles
2215        self.browser.getLink("My Roles").click()
2216        self.assertMatches(
2217            '...<div>Transcript Officer</div>...',
2218            self.browser.contents)
2219        # Officer can search for students in state 'transcripr requested'
2220        self.browser.open(self.container_path)
2221        self.browser.getControl(name="searchtype").value = ['transcript']
2222        self.browser.getControl("Find student(s)").click()
2223        self.assertTrue('Anna Tester' in self.browser.contents)
2224        self.browser.getLink("K1000000").click()
2225        self.browser.getLink("Manage transcript request").click()
2226        self.assertTrue(' UTC K1000000 wrote:<br><br>Comment line 1 <br>'
2227        'Comment line2<br><br>Dispatch Address:<br>Address line 1 <br>'
2228        'Address line2<br><br></p>' in self.browser.contents)
2229        self.browser.getControl(name="comment").value = (
2230            'Hello,\nYour transcript has been sent to the address provided.')
2231        self.browser.getControl("Save comment and mark as processed").click()
2232        self.assertTrue(
2233            'UTC mrtranscript wrote:\n\nHello,\nYour transcript has '
2234            'been sent to the address provided.\n\n'
2235            in self.student.transcript_comment)
2236        # The comment has been logged
2237        logfile = os.path.join(
2238            self.app['datacenter'].storage, 'logs', 'students.log')
2239        logcontent = open(logfile).read()
2240        self.assertTrue(
2241            'mrtranscript - students.browser.StudentTranscriptRequestProcessFormPage - '
2242            'K1000000 - comment: Hello,<br>'
2243            'Your transcript has been sent to the address provided'
2244            in logcontent)
2245
2246class StudentUITests(StudentsFullSetup):
2247    # Tests for Student class views and pages
2248
2249    def test_student_change_password(self):
2250        # Students can change the password
2251        self.student.personal_updated = datetime.utcnow()
2252        self.browser.open(self.login_path)
2253        self.browser.getControl(name="form.login").value = self.student_id
2254        self.browser.getControl(name="form.password").value = 'spwd'
2255        self.browser.getControl("Login").click()
2256        self.assertEqual(self.browser.url, self.student_path)
2257        self.assertTrue('You logged in' in self.browser.contents)
2258        # Change password
2259        self.browser.getLink("Change password").click()
2260        self.browser.getControl(name="change_password").value = 'pw'
2261        self.browser.getControl(
2262            name="change_password_repeat").value = 'pw'
2263        self.browser.getControl("Save").click()
2264        self.assertTrue('Password must have at least' in self.browser.contents)
2265        self.browser.getControl(name="change_password").value = 'new_password'
2266        self.browser.getControl(
2267            name="change_password_repeat").value = 'new_passssword'
2268        self.browser.getControl("Save").click()
2269        self.assertTrue('Passwords do not match' in self.browser.contents)
2270        self.browser.getControl(name="change_password").value = 'new_password'
2271        self.browser.getControl(
2272            name="change_password_repeat").value = 'new_password'
2273        self.browser.getControl("Save").click()
2274        self.assertTrue('Password changed' in self.browser.contents)
2275        # We are still logged in. Changing the password hasn't thrown us out.
2276        self.browser.getLink("Base Data").click()
2277        self.assertEqual(self.browser.url, self.student_path)
2278        # We can logout
2279        self.browser.getLink("Logout").click()
2280        self.assertTrue('You have been logged out' in self.browser.contents)
2281        self.assertEqual(self.browser.url, 'http://localhost/app/index')
2282        # We can login again with the new password
2283        self.browser.getLink("Login").click()
2284        self.browser.open(self.login_path)
2285        self.browser.getControl(name="form.login").value = self.student_id
2286        self.browser.getControl(name="form.password").value = 'new_password'
2287        self.browser.getControl("Login").click()
2288        self.assertEqual(self.browser.url, self.student_path)
2289        self.assertTrue('You logged in' in self.browser.contents)
2290        return
2291
2292    def test_forbidden_name(self):
2293        self.student.lastname = u'<TAG>Tester</TAG>'
2294        self.browser.open(self.login_path)
2295        self.browser.getControl(name="form.login").value = self.student_id
2296        self.browser.getControl(name="form.password").value = 'spwd'
2297        self.browser.getControl("Login").click()
2298        self.assertTrue('XXX: Base Data' in self.browser.contents)
2299        self.assertTrue('&lt;TAG&gt;Tester&lt;/TAG&gt;' in self.browser.contents)
2300        self.assertFalse('<TAG>Tester</TAG>' in self.browser.contents)
2301        return
2302
2303    def test_setpassword(self):
2304        # Set password for first-time access
2305        student = Student()
2306        student.reg_number = u'123456'
2307        student.firstname = u'Klaus'
2308        student.lastname = u'Tester'
2309        self.app['students'].addStudent(student)
2310        setpassword_path = 'http://localhost/app/setpassword'
2311        student_path = 'http://localhost/app/students/%s' % student.student_id
2312        self.browser.open(setpassword_path)
2313        self.browser.getControl(name="ac_series").value = self.existing_pwdseries
2314        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
2315        self.browser.getControl(name="reg_number").value = '223456'
2316        self.browser.getControl("Set").click()
2317        self.assertMatches('...No student found...',
2318                           self.browser.contents)
2319        self.browser.getControl(name="reg_number").value = '123456'
2320        self.browser.getControl(name="ac_number").value = '999999'
2321        self.browser.getControl("Set").click()
2322        self.assertMatches('...Access code is invalid...',
2323                           self.browser.contents)
2324        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
2325        self.browser.getControl("Set").click()
2326        self.assertMatches('...Password has been set. Your Student Id is...',
2327                           self.browser.contents)
2328        self.browser.getControl("Set").click()
2329        self.assertMatches(
2330            '...Password has already been set. Your Student Id is...',
2331            self.browser.contents)
2332        existing_pwdpin = self.pwdpins[1]
2333        parts = existing_pwdpin.split('-')[1:]
2334        existing_pwdseries, existing_pwdnumber = parts
2335        self.browser.getControl(name="ac_series").value = existing_pwdseries
2336        self.browser.getControl(name="ac_number").value = existing_pwdnumber
2337        self.browser.getControl(name="reg_number").value = '123456'
2338        self.browser.getControl("Set").click()
2339        self.assertMatches(
2340            '...You are using the wrong Access Code...',
2341            self.browser.contents)
2342        # The student can login with the new credentials
2343        self.browser.open(self.login_path)
2344        self.browser.getControl(name="form.login").value = student.student_id
2345        self.browser.getControl(
2346            name="form.password").value = self.existing_pwdnumber
2347        self.browser.getControl("Login").click()
2348        self.assertEqual(self.browser.url, student_path)
2349        self.assertTrue('You logged in' in self.browser.contents)
2350        return
2351
2352    def test_student_login(self):
2353        # Student cant login if their password is not set
2354        self.student.password = None
2355        self.browser.open(self.login_path)
2356        self.browser.getControl(name="form.login").value = self.student_id
2357        self.browser.getControl(name="form.password").value = 'spwd'
2358        self.browser.getControl("Login").click()
2359        self.assertTrue(
2360            'You entered invalid credentials.' in self.browser.contents)
2361        # We set the password again
2362        IUserAccount(
2363            self.app['students'][self.student_id]).setPassword('spwd')
2364        # Students can't login if their account is suspended/deactivated
2365        self.student.suspended = True
2366        self.browser.open(self.login_path)
2367        self.browser.getControl(name="form.login").value = self.student_id
2368        self.browser.getControl(name="form.password").value = 'spwd'
2369        self.browser.getControl("Login").click()
2370        self.assertMatches(
2371            '...<div class="alert alert-warning">'
2372            'Your account has been deactivated.</div>...', self.browser.contents)
2373        # If suspended_comment is set this message will be flashed instead
2374        self.student.suspended_comment = u'Aetsch baetsch!'
2375        self.browser.getControl(name="form.login").value = self.student_id
2376        self.browser.getControl(name="form.password").value = 'spwd'
2377        self.browser.getControl("Login").click()
2378        self.assertMatches(
2379            '...<div class="alert alert-warning">Aetsch baetsch!</div>...',
2380            self.browser.contents)
2381        self.student.suspended = False
2382        # Students can't login if a temporary password has been set and
2383        # is not expired
2384        self.app['students'][self.student_id].setTempPassword(
2385            'anybody', 'temp_spwd')
2386        self.browser.open(self.login_path)
2387        self.browser.getControl(name="form.login").value = self.student_id
2388        self.browser.getControl(name="form.password").value = 'spwd'
2389        self.browser.getControl("Login").click()
2390        self.assertMatches(
2391            '...Your account has been temporarily deactivated...',
2392            self.browser.contents)
2393        # The student can login with the temporary password
2394        self.browser.open(self.login_path)
2395        self.browser.getControl(name="form.login").value = self.student_id
2396        self.browser.getControl(name="form.password").value = 'temp_spwd'
2397        self.browser.getControl("Login").click()
2398        self.assertMatches(
2399            '...You logged in...', self.browser.contents)
2400        # Student can view the base data
2401        self.browser.open(self.student_path)
2402        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2403        self.assertEqual(self.browser.url, self.student_path)
2404        # When the password expires ...
2405        delta = timedelta(minutes=11)
2406        self.app['students'][self.student_id].temp_password[
2407            'timestamp'] = datetime.utcnow() - delta
2408        self.app['students'][self.student_id]._p_changed = True
2409        # ... the student will be automatically logged out
2410        self.assertRaises(
2411            Unauthorized, self.browser.open, self.student_path)
2412        # Then the student can login with the original password
2413        self.browser.open(self.login_path)
2414        self.browser.getControl(name="form.login").value = self.student_id
2415        self.browser.getControl(name="form.password").value = 'spwd'
2416        self.browser.getControl("Login").click()
2417        self.assertMatches(
2418            '...You logged in...', self.browser.contents)
2419
2420    def test_maintenance_mode(self):
2421        config = grok.getSite()['configuration']
2422        self.browser.open(self.login_path)
2423        self.browser.getControl(name="form.login").value = self.student_id
2424        self.browser.getControl(name="form.password").value = 'spwd'
2425        self.browser.getControl("Login").click()
2426        # Student logged in.
2427        self.assertTrue('You logged in' in self.browser.contents)
2428        self.assertTrue("Anna Tester" in self.browser.contents)
2429        # If maintenance mode is enabled, student is immediately logged out.
2430        config.maintmode_enabled_by = u'any_user'
2431        self.assertRaises(
2432            Unauthorized, self.browser.open, 'http://localhost/app/faculties')
2433        self.browser.open('http://localhost/app/login')
2434        self.assertTrue('The portal is in maintenance mode' in self.browser.contents)
2435        # Student really can't login if maintenance mode is enabled.
2436        self.browser.open(self.login_path)
2437        self.browser.getControl(name="form.login").value = self.student_id
2438        self.browser.getControl(name="form.password").value = 'spwd'
2439        self.browser.getControl("Login").click()
2440        # A second warning is raised.
2441        self.assertTrue(
2442            'The portal is in maintenance mode. You can\'t login!'
2443            in self.browser.contents)
2444        return
2445
2446    def test_student_clearance(self):
2447        # Student cant login if their password is not set
2448        IWorkflowInfo(self.student).fireTransition('admit')
2449        self.browser.open(self.login_path)
2450        self.browser.getControl(name="form.login").value = self.student_id
2451        self.browser.getControl(name="form.password").value = 'spwd'
2452        self.browser.getControl("Login").click()
2453        self.assertMatches(
2454            '...You logged in...', self.browser.contents)
2455        # Admitted student can upload a passport picture
2456        self.browser.open(self.student_path + '/change_portrait')
2457        ctrl = self.browser.getControl(name='passportuploadedit')
2458        file_obj = open(SAMPLE_IMAGE, 'rb')
2459        file_ctrl = ctrl.mech_control
2460        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
2461        self.browser.getControl(
2462            name='upload_passportuploadedit').click()
2463        self.assertTrue(
2464            'src="http://localhost/app/students/K1000000/passport.jpg"'
2465            in self.browser.contents)
2466        # Students can open admission letter
2467        self.browser.getLink("Base Data").click()
2468        self.browser.getLink("Download admission letter").click()
2469        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2470        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2471        # Student can view the clearance data
2472        self.browser.open(self.student_path)
2473        self.browser.getLink("Clearance Data").click()
2474        # Student can't open clearance edit form before starting clearance
2475        self.browser.open(self.student_path + '/cedit')
2476        self.assertMatches('...The requested form is locked...',
2477                           self.browser.contents)
2478        self.browser.getLink("Clearance Data").click()
2479        self.browser.getLink("Start clearance").click()
2480        self.student.phone = None
2481        # Uups, we forgot to fill the phone fields
2482        self.browser.getControl("Start clearance").click()
2483        self.assertMatches('...Phone number is missing...',
2484                           self.browser.contents)
2485        self.browser.open(self.student_path + '/edit_base')
2486        self.browser.getControl(name="form.phone.ext").value = '12345'
2487        self.browser.getControl("Save").click()
2488        self.browser.open(self.student_path + '/start_clearance')
2489        self.browser.getControl(name="ac_series").value = '3'
2490        self.browser.getControl(name="ac_number").value = '4444444'
2491        self.browser.getControl("Start clearance now").click()
2492        self.assertMatches('...Activation code is invalid...',
2493                           self.browser.contents)
2494        self.browser.getControl(name="ac_series").value = self.existing_clrseries
2495        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
2496        # Owner is Hans Wurst, AC can't be invalidated
2497        self.browser.getControl("Start clearance now").click()
2498        self.assertMatches('...You are not the owner of this access code...',
2499                           self.browser.contents)
2500        # Set the correct owner
2501        self.existing_clrac.owner = self.student_id
2502        # clr_code might be set (and thus returns None) due importing
2503        # an empty clr_code column.
2504        self.student.clr_code = None
2505        self.browser.getControl("Start clearance now").click()
2506        self.assertMatches('...Clearance process has been started...',
2507                           self.browser.contents)
2508        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
2509        self.browser.getControl("Save", index=0).click()
2510        # Student can view the clearance data
2511        self.browser.getLink("Clearance Data").click()
2512        # and go back to the edit form
2513        self.browser.getLink("Edit").click()
2514        # Students can upload documents
2515        ctrl = self.browser.getControl(name='birthcertificateupload')
2516        file_obj = open(SAMPLE_IMAGE, 'rb')
2517        file_ctrl = ctrl.mech_control
2518        file_ctrl.add_file(file_obj, filename='my_birth_certificate.jpg')
2519        self.browser.getControl(
2520            name='upload_birthcertificateupload').click()
2521        self.assertTrue(
2522            'href="http://localhost/app/students/K1000000/birth_certificate"'
2523            in self.browser.contents)
2524        # Students can open clearance slip
2525        self.browser.getLink("View").click()
2526        self.browser.getLink("Download clearance slip").click()
2527        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2528        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2529        # Students can request clearance
2530        self.browser.open(self.edit_clearance_path)
2531        self.browser.getControl("Save and request clearance").click()
2532        self.browser.getControl(name="ac_series").value = self.existing_clrseries
2533        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
2534        self.browser.getControl("Request clearance now").click()
2535        self.assertMatches('...Clearance has been requested...',
2536                           self.browser.contents)
2537        # Student can't reopen clearance form after requesting clearance
2538        self.browser.open(self.student_path + '/cedit')
2539        self.assertMatches('...The requested form is locked...',
2540                           self.browser.contents)
2541
2542    def test_student_course_registration(self):
2543        # Student cant login if their password is not set
2544        IWorkflowInfo(self.student).fireTransition('admit')
2545        self.browser.open(self.login_path)
2546        self.browser.getControl(name="form.login").value = self.student_id
2547        self.browser.getControl(name="form.password").value = 'spwd'
2548        self.browser.getControl("Login").click()
2549        # Student can't add study level if not in state 'school fee paid'
2550        self.browser.open(self.student_path + '/studycourse/add')
2551        self.assertMatches('...The requested form is locked...',
2552                           self.browser.contents)
2553        # ... and must be transferred first
2554        IWorkflowState(self.student).setState('school fee paid')
2555        # Now students can add the current study level
2556        self.browser.getLink("Study Course").click()
2557        self.student['studycourse'].current_level = None
2558        self.browser.getLink("Add course list").click()
2559        self.assertMatches('...Your data are incomplete...',
2560                           self.browser.contents)
2561        self.student['studycourse'].current_level = 100
2562        self.browser.getLink("Add course list").click()
2563        self.assertMatches('...Add current level 100 (Year 1)...',
2564                           self.browser.contents)
2565        self.browser.getControl("Create course list now").click()
2566        # A level with one course ticket was created
2567        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
2568        self.browser.getLink("100").click()
2569        self.browser.getLink("Edit course list").click()
2570        self.browser.getLink("here").click()
2571        self.browser.getControl(name="form.course").value = ['COURSE1']
2572        self.browser.getControl("Add course ticket").click()
2573        self.assertMatches('...The ticket exists...',
2574                           self.browser.contents)
2575        self.student['studycourse'].current_level = 200
2576        self.browser.getLink("Study Course").click()
2577        self.browser.getLink("Add course list").click()
2578        self.assertMatches('...Add current level 200 (Year 2)...',
2579                           self.browser.contents)
2580        self.browser.getControl("Create course list now").click()
2581        self.browser.getLink("200").click()
2582        self.browser.getLink("Edit course list").click()
2583        self.browser.getLink("here").click()
2584        self.browser.getControl(name="form.course").value = ['COURSE1']
2585        self.course.credits = 100
2586        self.browser.getControl("Add course ticket").click()
2587        self.assertMatches(
2588            '...Total credits exceed 50...', self.browser.contents)
2589        self.course.credits = 10
2590        self.browser.getControl("Add course ticket").click()
2591        self.assertMatches('...The ticket exists...',
2592                           self.browser.contents)
2593        # Indeed the ticket exists as carry-over course from level 100
2594        # since its score was 0
2595        self.assertTrue(
2596            self.student['studycourse']['200']['COURSE1'].carry_over is True)
2597        # Students can open the pdf course registration slip
2598        self.browser.open(self.student_path + '/studycourse/200')
2599        self.browser.getLink("Download course registration slip").click()
2600        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2601        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2602        # Students can remove course tickets
2603        self.browser.open(self.student_path + '/studycourse/200/edit')
2604        self.browser.getControl("Remove selected", index=0).click()
2605        self.assertTrue('No ticket selected' in self.browser.contents)
2606        # No ticket can be selected since the carry-over course is a core course
2607        self.assertRaises(
2608            LookupError, self.browser.getControl, name='val_id')
2609        self.student['studycourse']['200']['COURSE1'].mandatory = False
2610        self.browser.open(self.student_path + '/studycourse/200/edit')
2611        # Course list can't be registered if total_credits exceeds max_credits
2612        self.student['studycourse']['200']['COURSE1'].credits = 60
2613        self.browser.getControl("Register course list").click()
2614        self.assertTrue('Maximum credits of 50 exceeded' in self.browser.contents)
2615        # Student can now remove the ticket
2616        ctrl = self.browser.getControl(name='val_id')
2617        ctrl.getControl(value='COURSE1').selected = True
2618        self.browser.getControl("Remove selected", index=0).click()
2619        self.assertTrue('Successfully removed' in self.browser.contents)
2620        # Removing course tickets is properly logged
2621        logfile = os.path.join(
2622            self.app['datacenter'].storage, 'logs', 'students.log')
2623        logcontent = open(logfile).read()
2624        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage '
2625        '- K1000000 - removed: COURSE1 at 200' in logcontent)
2626        # They can add the same ticket using the edit page directly.
2627        # We can do the same by adding the course on the manage page directly
2628        self.browser.getControl(name="course").value = 'COURSE1'
2629        self.browser.getControl("Add course ticket").click()
2630        # Adding course tickets is logged
2631        logfile = os.path.join(
2632            self.app['datacenter'].storage, 'logs', 'students.log')
2633        logcontent = open(logfile).read()
2634        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage - '
2635            'K1000000 - added: COURSE1|200|2004' in logcontent)
2636        # Course list can be registered
2637        self.browser.getControl("Register course list").click()
2638        self.assertTrue('Course list has been registered' in self.browser.contents)
2639        self.assertEqual(self.student.state, 'courses registered')
2640        # Course list can be unregistered
2641        self.browser.getLink("Unregister courses").click()
2642        self.assertEqual(self.student.state, 'school fee paid')
2643        self.assertTrue('Course list has been unregistered' in self.browser.contents)
2644        self.browser.open(self.student_path + '/studycourse/200/unregister_courses')
2645        self.assertTrue('You are in the wrong state' in self.browser.contents)
2646        # Students can view the transcript
2647        #self.browser.open(self.studycourse_path)
2648        #self.browser.getLink("Transcript").click()
2649        #self.browser.getLink("Academic Transcript").click()
2650        #self.assertEqual(self.browser.headers['Status'], '200 Ok')
2651        #self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2652        return
2653
2654    def test_postgraduate_student_access(self):
2655        self.certificate.study_mode = 'pg_ft'
2656        self.certificate.start_level = 999
2657        self.certificate.end_level = 999
2658        self.student['studycourse'].current_level = 999
2659        IWorkflowState(self.student).setState('school fee paid')
2660        self.browser.open(self.login_path)
2661        self.browser.getControl(name="form.login").value = self.student_id
2662        self.browser.getControl(name="form.password").value = 'spwd'
2663        self.browser.getControl("Login").click()
2664        self.assertTrue(
2665            'You logged in.' in self.browser.contents)
2666        # Now students can add the current study level
2667        self.browser.getLink("Study Course").click()
2668        self.browser.getLink("Add course list").click()
2669        self.assertMatches('...Add current level Postgraduate Level...',
2670                           self.browser.contents)
2671        self.browser.getControl("Create course list now").click()
2672        # A level with one course ticket was created
2673        self.assertEqual(self.student['studycourse']['999'].number_of_tickets, 0)
2674        self.browser.getLink("999").click()
2675        self.browser.getLink("Edit course list").click()
2676        self.browser.getLink("here").click()
2677        self.browser.getControl(name="form.course").value = ['COURSE1']
2678        self.browser.getControl("Add course ticket").click()
2679        self.assertMatches('...Successfully added COURSE1...',
2680                           self.browser.contents)
2681        # Postgraduate students can't register course lists
2682        self.browser.getControl("Register course list").click()
2683        self.assertTrue("your course list can't bee registered"
2684            in self.browser.contents)
2685        self.assertEqual(self.student.state, 'school fee paid')
2686        return
2687
2688    def test_student_clearance_wo_clrcode(self):
2689        IWorkflowState(self.student).setState('clearance started')
2690        self.browser.open(self.login_path)
2691        self.browser.getControl(name="form.login").value = self.student_id
2692        self.browser.getControl(name="form.password").value = 'spwd'
2693        self.browser.getControl("Login").click()
2694        self.browser.open(self.edit_clearance_path)
2695        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
2696        self.browser.getControl("Save and request clearance").click()
2697        self.assertMatches('...Clearance has been requested...',
2698                           self.browser.contents)
2699
2700    def test_student_clearance_payment(self):
2701        # Login
2702        self.browser.open(self.login_path)
2703        self.browser.getControl(name="form.login").value = self.student_id
2704        self.browser.getControl(name="form.password").value = 'spwd'
2705        self.browser.getControl("Login").click()
2706
2707        # Students can add online clearance payment tickets
2708        self.browser.open(self.payments_path + '/addop')
2709        self.browser.getControl(name="form.p_category").value = ['clearance']
2710        self.browser.getControl("Create ticket").click()
2711        self.assertMatches('...ticket created...',
2712                           self.browser.contents)
2713
2714        # Students can't approve the payment
2715        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
2716        ctrl = self.browser.getControl(name='val_id')
2717        value = ctrl.options[0]
2718        self.browser.getLink(value).click()
2719        payment_url = self.browser.url
2720        self.assertRaises(
2721            Unauthorized, self.browser.open, payment_url + '/approve')
2722        # In the base package they can 'use' a fake approval view.
2723        # XXX: I tried to use
2724        # self.student['payments'][value].approveStudentPayment() instead.
2725        # But this function fails in
2726        # w.k.accesscodes.accesscode.create_accesscode.
2727        # grok.getSite returns None in tests.
2728        self.browser.open(payment_url + '/fake_approve')
2729        self.assertMatches('...Payment approved...',
2730                          self.browser.contents)
2731        expected = '''...
2732        <td>
2733          <span>Paid</span>
2734        </td>...'''
2735        expected = '''...
2736        <td>
2737          <span>Paid</span>
2738        </td>...'''
2739        self.assertMatches(expected,self.browser.contents)
2740        payment_id = self.student['payments'].keys()[0]
2741        payment = self.student['payments'][payment_id]
2742        self.assertEqual(payment.p_state, 'paid')
2743        self.assertEqual(payment.r_amount_approved, 3456.0)
2744        self.assertEqual(payment.r_code, 'AP')
2745        self.assertEqual(payment.r_desc, u'Payment approved by Anna Tester')
2746        # The new CLR-0 pin has been created
2747        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
2748        pin = self.app['accesscodes']['CLR-0'].keys()[0]
2749        ac = self.app['accesscodes']['CLR-0'][pin]
2750        self.assertEqual(ac.owner, self.student_id)
2751        self.assertEqual(ac.cost, 3456.0)
2752
2753        # Students can open the pdf payment slip
2754        self.browser.open(payment_url + '/payment_slip.pdf')
2755        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2756        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2757
2758        # The new CLR-0 pin can be used for starting clearance
2759        # but they have to upload a passport picture first
2760        # which is only possible in state admitted
2761        self.browser.open(self.student_path + '/change_portrait')
2762        self.assertMatches('...form is locked...',
2763                          self.browser.contents)
2764        IWorkflowInfo(self.student).fireTransition('admit')
2765        self.browser.open(self.student_path + '/change_portrait')
2766        image = open(SAMPLE_IMAGE, 'rb')
2767        ctrl = self.browser.getControl(name='passportuploadedit')
2768        file_ctrl = ctrl.mech_control
2769        file_ctrl.add_file(image, filename='my_photo.jpg')
2770        self.browser.getControl(
2771            name='upload_passportuploadedit').click()
2772        self.browser.open(self.student_path + '/start_clearance')
2773        parts = pin.split('-')[1:]
2774        clrseries, clrnumber = parts
2775        self.browser.getControl(name="ac_series").value = clrseries
2776        self.browser.getControl(name="ac_number").value = clrnumber
2777        self.browser.getControl("Start clearance now").click()
2778        self.assertMatches('...Clearance process has been started...',
2779                           self.browser.contents)
2780
2781    def test_student_schoolfee_payment(self):
2782        configuration = createObject('waeup.SessionConfiguration')
2783        configuration.academic_session = 2005
2784        self.app['configuration'].addSessionConfiguration(configuration)
2785        # Login
2786        self.browser.open(self.login_path)
2787        self.browser.getControl(name="form.login").value = self.student_id
2788        self.browser.getControl(name="form.password").value = 'spwd'
2789        self.browser.getControl("Login").click()
2790
2791        # Students can add online school fee payment tickets.
2792        IWorkflowState(self.student).setState('returning')
2793        self.browser.open(self.payments_path)
2794        self.assertRaises(
2795            LookupError, self.browser.getControl, name='val_id')
2796        self.browser.getLink("Add current session payment ticket").click()
2797        self.browser.getControl(name="form.p_category").value = ['schoolfee']
2798        self.browser.getControl("Create ticket").click()
2799        self.assertMatches('...ticket created...',
2800                           self.browser.contents)
2801        ctrl = self.browser.getControl(name='val_id')
2802        value = ctrl.options[0]
2803        self.browser.getLink(value).click()
2804        self.assertMatches('...Amount Authorized...',
2805                           self.browser.contents)
2806        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
2807        # Payment session and will be calculated as defined
2808        # in w.k.students.utils because we set changed the state
2809        # to returning
2810        self.assertEqual(self.student['payments'][value].p_session, 2005)
2811        self.assertEqual(self.student['payments'][value].p_level, 200)
2812
2813        # Student is the payer of the payment ticket.
2814        payer = IPayer(self.student['payments'][value])
2815        self.assertEqual(payer.display_fullname, 'Anna Tester')
2816        self.assertEqual(payer.id, self.student_id)
2817        self.assertEqual(payer.faculty, 'fac1')
2818        self.assertEqual(payer.department, 'dep1')
2819
2820        # We simulate the approval
2821        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
2822        self.browser.open(self.browser.url + '/fake_approve')
2823        self.assertMatches('...Payment approved...',
2824                          self.browser.contents)
2825
2826        ## The new SFE-0 pin can be used for starting new session
2827        #self.browser.open(self.studycourse_path)
2828        #self.browser.getLink('Start new session').click()
2829        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
2830        #parts = pin.split('-')[1:]
2831        #sfeseries, sfenumber = parts
2832        #self.browser.getControl(name="ac_series").value = sfeseries
2833        #self.browser.getControl(name="ac_number").value = sfenumber
2834        #self.browser.getControl("Start now").click()
2835        #self.assertMatches('...Session started...',
2836        #                   self.browser.contents)
2837
2838        self.assertTrue(self.student.state == 'school fee paid')
2839        return
2840
2841    def test_student_bedallocation_payment(self):
2842        # Login
2843        self.browser.open(self.login_path)
2844        self.browser.getControl(name="form.login").value = self.student_id
2845        self.browser.getControl(name="form.password").value = 'spwd'
2846        self.browser.getControl("Login").click()
2847        self.browser.open(self.payments_path)
2848        self.browser.open(self.payments_path + '/addop')
2849        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
2850        self.browser.getControl("Create ticket").click()
2851        self.assertMatches('...ticket created...',
2852                           self.browser.contents)
2853        # Students can remove only online payment tickets which have
2854        # not received a valid callback
2855        self.browser.open(self.payments_path)
2856        ctrl = self.browser.getControl(name='val_id')
2857        value = ctrl.options[0]
2858        ctrl.getControl(value=value).selected = True
2859        self.browser.getControl("Remove selected", index=0).click()
2860        self.assertTrue('Successfully removed' in self.browser.contents)
2861
2862    def test_student_maintenance_payment(self):
2863        # Login
2864        self.browser.open(self.login_path)
2865        self.browser.getControl(name="form.login").value = self.student_id
2866        self.browser.getControl(name="form.password").value = 'spwd'
2867        self.browser.getControl("Login").click()
2868        self.browser.open(self.payments_path)
2869        self.browser.open(self.payments_path + '/addop')
2870        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
2871        self.browser.getControl("Create ticket").click()
2872        self.assertMatches('...You have not yet booked accommodation...',
2873                           self.browser.contents)
2874        # We continue this test in test_student_accommodation
2875
2876    def test_student_previous_payments(self):
2877        configuration = createObject('waeup.SessionConfiguration')
2878        configuration.academic_session = 2000
2879        configuration.clearance_fee = 3456.0
2880        configuration.booking_fee = 123.4
2881        self.app['configuration'].addSessionConfiguration(configuration)
2882        configuration2 = createObject('waeup.SessionConfiguration')
2883        configuration2.academic_session = 2003
2884        configuration2.clearance_fee = 3456.0
2885        configuration2.booking_fee = 123.4
2886        self.app['configuration'].addSessionConfiguration(configuration2)
2887        configuration3 = createObject('waeup.SessionConfiguration')
2888        configuration3.academic_session = 2005
2889        configuration3.clearance_fee = 3456.0
2890        configuration3.booking_fee = 123.4
2891        self.app['configuration'].addSessionConfiguration(configuration3)
2892        self.student['studycourse'].entry_session = 2002
2893
2894        # Login
2895        self.browser.open(self.login_path)
2896        self.browser.getControl(name="form.login").value = self.student_id
2897        self.browser.getControl(name="form.password").value = 'spwd'
2898        self.browser.getControl("Login").click()
2899
2900        # Students can add previous school fee payment tickets in any state.
2901        IWorkflowState(self.student).setState('courses registered')
2902        self.browser.open(self.payments_path)
2903        self.browser.getLink("Add previous session payment ticket").click()
2904
2905        # Previous session payment form is provided
2906        self.assertEqual(self.student.current_session, 2004)
2907        self.browser.getControl(name="form.p_category").value = ['schoolfee']
2908        self.browser.getControl(name="form.p_session").value = ['2000']
2909        self.browser.getControl(name="form.p_level").value = ['300']
2910        self.browser.getControl("Create ticket").click()
2911        self.assertMatches('...The previous session must not fall below...',
2912                           self.browser.contents)
2913        self.browser.getControl(name="form.p_category").value = ['schoolfee']
2914        self.browser.getControl(name="form.p_session").value = ['2005']
2915        self.browser.getControl(name="form.p_level").value = ['300']
2916        self.browser.getControl("Create ticket").click()
2917        self.assertMatches('...This is not a previous session...',
2918                           self.browser.contents)
2919        # Students can pay current session school fee.
2920        self.browser.getControl(name="form.p_category").value = ['schoolfee']
2921        self.browser.getControl(name="form.p_session").value = ['2004']
2922        self.browser.getControl(name="form.p_level").value = ['300']
2923        self.browser.getControl("Create ticket").click()
2924        self.assertMatches('...ticket created...',
2925                           self.browser.contents)
2926        ctrl = self.browser.getControl(name='val_id')
2927        value = ctrl.options[0]
2928        self.browser.getLink(value).click()
2929        self.assertMatches('...Amount Authorized...',
2930                           self.browser.contents)
2931        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
2932
2933        # Payment session is properly set
2934        self.assertEqual(self.student['payments'][value].p_session, 2004)
2935        self.assertEqual(self.student['payments'][value].p_level, 300)
2936
2937        # We simulate the approval
2938        self.browser.open(self.browser.url + '/fake_approve')
2939        self.assertMatches('...Payment approved...',
2940                          self.browser.contents)
2941
2942        # No AC has been created
2943        self.assertEqual(len(self.app['accesscodes']['SFE-0'].keys()), 0)
2944        self.assertTrue(self.student['payments'][value].ac is None)
2945
2946        # Current payment flag is set False
2947        self.assertFalse(self.student['payments'][value].p_current)
2948
2949        # Button and form are not available for students who are in
2950        # states up to cleared
2951        self.student['studycourse'].entry_session = 2004
2952        IWorkflowState(self.student).setState('cleared')
2953        self.browser.open(self.payments_path)
2954        self.assertFalse(
2955            "Add previous session payment ticket" in self.browser.contents)
2956        self.browser.open(self.payments_path + '/addpp')
2957        self.assertTrue(
2958            "No previous payment to be made" in self.browser.contents)
2959        return
2960
2961    def test_postgraduate_student_payments(self):
2962        configuration = createObject('waeup.SessionConfiguration')
2963        configuration.academic_session = 2005
2964        self.app['configuration'].addSessionConfiguration(configuration)
2965        self.certificate.study_mode = 'pg_ft'
2966        self.certificate.start_level = 999
2967        self.certificate.end_level = 999
2968        self.student['studycourse'].current_level = 999
2969        # Login
2970        self.browser.open(self.login_path)
2971        self.browser.getControl(name="form.login").value = self.student_id
2972        self.browser.getControl(name="form.password").value = 'spwd'
2973        self.browser.getControl("Login").click()
2974        # Students can add online school fee payment tickets.
2975        IWorkflowState(self.student).setState('cleared')
2976        self.browser.open(self.payments_path)
2977        self.browser.getLink("Add current session payment ticket").click()
2978        self.browser.getControl(name="form.p_category").value = ['schoolfee']
2979        self.browser.getControl("Create ticket").click()
2980        self.assertMatches('...ticket created...',
2981                           self.browser.contents)
2982        ctrl = self.browser.getControl(name='val_id')
2983        value = ctrl.options[0]
2984        self.browser.getLink(value).click()
2985        self.assertMatches('...Amount Authorized...',
2986                           self.browser.contents)
2987        # Payment session and level are current ones.
2988        # Postgrads have to pay school_fee_1.
2989        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
2990        self.assertEqual(self.student['payments'][value].p_session, 2004)
2991        self.assertEqual(self.student['payments'][value].p_level, 999)
2992
2993        # We simulate the approval
2994        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
2995        self.browser.open(self.browser.url + '/fake_approve')
2996        self.assertMatches('...Payment approved...',
2997                          self.browser.contents)
2998
2999        ## The new SFE-0 pin can be used for starting session
3000        #self.browser.open(self.studycourse_path)
3001        #self.browser.getLink('Start new session').click()
3002        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
3003        #parts = pin.split('-')[1:]
3004        #sfeseries, sfenumber = parts
3005        #self.browser.getControl(name="ac_series").value = sfeseries
3006        #self.browser.getControl(name="ac_number").value = sfenumber
3007        #self.browser.getControl("Start now").click()
3008        #self.assertMatches('...Session started...',
3009        #                   self.browser.contents)
3010
3011        self.assertTrue(self.student.state == 'school fee paid')
3012
3013        # Postgrad students do not need to register courses the
3014        # can just pay for the next session.
3015        self.browser.open(self.payments_path)
3016        # Remove first payment to be sure that we access the right ticket
3017        del self.student['payments'][value]
3018        self.browser.getLink("Add current session payment ticket").click()
3019        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3020        self.browser.getControl("Create ticket").click()
3021        ctrl = self.browser.getControl(name='val_id')
3022        value = ctrl.options[0]
3023        self.browser.getLink(value).click()
3024        # Payment session has increased by one, payment level remains the same.
3025        # Returning Postgraduates have to pay school_fee_2.
3026        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
3027        self.assertEqual(self.student['payments'][value].p_session, 2005)
3028        self.assertEqual(self.student['payments'][value].p_level, 999)
3029
3030        # Student is still in old session
3031        self.assertEqual(self.student.current_session, 2004)
3032
3033        # We do not need to pay the ticket if any other
3034        # SFE pin is provided
3035        pin_container = self.app['accesscodes']
3036        pin_container.createBatch(
3037            datetime.utcnow(), 'some_userid', 'SFE', 9.99, 1)
3038        pin = pin_container['SFE-1'].values()[0].representation
3039        sfeseries, sfenumber = pin.split('-')[1:]
3040        # The new SFE-1 pin can be used for starting new session
3041        self.browser.open(self.studycourse_path)
3042        self.browser.getLink('Start new session').click()
3043        self.browser.getControl(name="ac_series").value = sfeseries
3044        self.browser.getControl(name="ac_number").value = sfenumber
3045        self.browser.getControl("Start now").click()
3046        self.assertMatches('...Session started...',
3047                           self.browser.contents)
3048        self.assertTrue(self.student.state == 'school fee paid')
3049        # Student is in new session
3050        self.assertEqual(self.student.current_session, 2005)
3051        self.assertEqual(self.student['studycourse'].current_level, 999)
3052        return
3053
3054    def test_student_accommodation(self):
3055        # Create a second hostel with one bed
3056        hostel = Hostel()
3057        hostel.hostel_id = u'hall-2'
3058        hostel.hostel_name = u'Hall 2'
3059        self.app['hostels'].addHostel(hostel)
3060        bed = Bed()
3061        bed.bed_id = u'hall-2_A_101_A'
3062        bed.bed_number = 1
3063        bed.owner = NOT_OCCUPIED
3064        bed.bed_type = u'regular_female_fr'
3065        self.app['hostels'][hostel.hostel_id].addBed(bed)
3066
3067        self.browser.open(self.login_path)
3068        self.browser.getControl(name="form.login").value = self.student_id
3069        self.browser.getControl(name="form.password").value = 'spwd'
3070        self.browser.getControl("Login").click()
3071        # Students can add online booking fee payment tickets and open the
3072        # callback view (see test_manage_payments).
3073        self.browser.getLink("Payments").click()
3074        self.browser.getLink("Add current session payment ticket").click()
3075        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
3076        self.browser.getControl("Create ticket").click()
3077        ctrl = self.browser.getControl(name='val_id')
3078        value = ctrl.options[0]
3079        self.browser.getLink(value).click()
3080        self.browser.open(self.browser.url + '/fake_approve')
3081        # The new HOS-0 pin has been created.
3082        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
3083        pin = self.app['accesscodes']['HOS-0'].keys()[0]
3084        ac = self.app['accesscodes']['HOS-0'][pin]
3085        parts = pin.split('-')[1:]
3086        sfeseries, sfenumber = parts
3087        # Students can use HOS code and book a bed space with it ...
3088        self.browser.open(self.acco_path)
3089        # ... but not if booking period has expired ...
3090        self.app['hostels'].enddate = datetime.now(pytz.utc)
3091        self.browser.getControl("Book accommodation").click()
3092        self.assertMatches('...Outside booking period: ...',
3093                           self.browser.contents)
3094        self.app['hostels'].enddate = datetime.now(pytz.utc) + timedelta(days=10)
3095        # ... or student data are incomplete ...
3096        self.student['studycourse'].current_level = None
3097        self.browser.getControl("Book accommodation").click()
3098        self.assertMatches('...Your data are incomplete...',
3099            self.browser.contents)
3100        self.student['studycourse'].current_level = 100
3101        # ... or student is not the an allowed state ...
3102        self.browser.getControl("Book accommodation").click()
3103        self.assertMatches('...You are in the wrong...',
3104                           self.browser.contents)
3105        # Students can still not see the disired hostel selector.
3106        self.assertFalse('desired hostel' in self.browser.contents)
3107        IWorkflowInfo(self.student).fireTransition('admit')
3108        # Students can now see the disired hostel selector.
3109        self.browser.reload()
3110        self.browser.open(self.acco_path)
3111        self.assertTrue('desired hostel' in self.browser.contents)
3112        self.browser.getControl(name="hostel").value = ['hall-2']
3113        self.browser.getControl("Save").click()
3114        self.assertTrue('selection has been saved' in self.browser.contents)
3115        self.assertTrue('<option selected="selected" value="hall-2">'
3116            in self.browser.contents)
3117        self.browser.getControl("Book accommodation").click()
3118        self.assertMatches('...Activation Code:...',
3119                           self.browser.contents)
3120        # Student can't use faked ACs ...
3121        self.browser.getControl(name="ac_series").value = u'nonsense'
3122        self.browser.getControl(name="ac_number").value = sfenumber
3123        self.browser.getControl("Create bed ticket").click()
3124        self.assertMatches('...Activation code is invalid...',
3125                           self.browser.contents)
3126        # ... or ACs owned by somebody else.
3127        ac.owner = u'Anybody'
3128        self.browser.getControl(name="ac_series").value = sfeseries
3129        self.browser.getControl(name="ac_number").value = sfenumber
3130        self.browser.getControl("Create bed ticket").click()
3131        # Hostel 2 has only a bed for women.
3132        self.assertTrue('There is no free bed in your desired hostel'
3133            in self.browser.contents)
3134        self.browser.getControl(name="hostel").value = ['hall-1']
3135        self.browser.getControl("Save").click()
3136        self.browser.getControl("Book accommodation").click()
3137        # Student can't use faked ACs ...
3138        self.browser.getControl(name="ac_series").value = sfeseries
3139        self.browser.getControl(name="ac_number").value = sfenumber
3140        self.browser.getControl("Create bed ticket").click()
3141        self.assertMatches('...You are not the owner of this access code...',
3142                           self.browser.contents)
3143        # The bed remains empty.
3144        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
3145        self.assertTrue(bed.owner == NOT_OCCUPIED)
3146        ac.owner = self.student_id
3147        self.browser.getControl(name="ac_series").value = sfeseries
3148        self.browser.getControl(name="ac_number").value = sfenumber
3149        self.browser.getControl("Create bed ticket").click()
3150        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
3151                           self.browser.contents)
3152        # Bed has been allocated.
3153        self.assertTrue(bed.owner == self.student_id)
3154        # BedTicketAddPage is now blocked.
3155        self.browser.getControl("Book accommodation").click()
3156        self.assertMatches('...You already booked a bed space...',
3157            self.browser.contents)
3158        # The bed ticket displays the data correctly.
3159        self.browser.open(self.acco_path + '/2004')
3160        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
3161                           self.browser.contents)
3162        self.assertMatches('...2004/2005...', self.browser.contents)
3163        self.assertMatches('...regular_male_fr...', self.browser.contents)
3164        self.assertMatches('...%s...' % pin, self.browser.contents)
3165        # Students can open the pdf slip.
3166        self.browser.open(self.browser.url + '/bed_allocation_slip.pdf')
3167        self.assertEqual(self.browser.headers['Status'], '200 Ok')
3168        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
3169        # Students can't relocate themselves.
3170        self.assertFalse('Relocate' in self.browser.contents)
3171        relocate_path = self.acco_path + '/2004/relocate'
3172        self.assertRaises(
3173            Unauthorized, self.browser.open, relocate_path)
3174        # Students can't see the Remove button and check boxes.
3175        self.browser.open(self.acco_path)
3176        self.assertFalse('Remove' in self.browser.contents)
3177        self.assertFalse('val_id' in self.browser.contents)
3178        # Students can pay maintenance fee now.
3179        self.browser.open(self.payments_path)
3180        self.browser.open(self.payments_path + '/addop')
3181        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
3182        self.browser.getControl("Create ticket").click()
3183        self.assertMatches('...Payment ticket created...',
3184                           self.browser.contents)
3185        ctrl = self.browser.getControl(name='val_id')
3186        value = ctrl.options[0]
3187        # Maintennace fee is taken from the hostel object.
3188        self.assertEqual(self.student['payments'][value].amount_auth, 876.0)
3189        # If the hostel's maintenance fee isn't set, the fee is
3190        # taken from the session configuration object.
3191        self.app['hostels']['hall-1'].maint_fee = 0.0
3192        self.browser.open(self.payments_path + '/addop')
3193        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
3194        self.browser.getControl("Create ticket").click()
3195        ctrl = self.browser.getControl(name='val_id')
3196        value = ctrl.options[1]
3197        self.assertEqual(self.student['payments'][value].amount_auth, 987.0)
3198        # The bedticket is aware of successfull maintenance fee payment
3199        bedticket = self.student['accommodation']['2004']
3200        self.assertFalse(bedticket.maint_payment_made)
3201        self.student['payments'][value].approve()
3202        self.assertTrue(bedticket.maint_payment_made)
3203        return
3204
3205    def test_change_password_request(self):
3206        self.browser.open('http://localhost/app/changepw')
3207        self.browser.getControl(name="form.identifier").value = '123'
3208        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
3209        self.browser.getControl("Send login credentials").click()
3210        self.assertTrue('An email with' in self.browser.contents)
3211
3212    def test_student_expired_personal_data(self):
3213        # Login
3214        IWorkflowState(self.student).setState('school fee paid')
3215        delta = timedelta(days=180)
3216        self.student.personal_updated = datetime.utcnow() - delta
3217        self.browser.open(self.login_path)
3218        self.browser.getControl(name="form.login").value = self.student_id
3219        self.browser.getControl(name="form.password").value = 'spwd'
3220        self.browser.getControl("Login").click()
3221        self.assertEqual(self.browser.url, self.student_path)
3222        self.assertTrue(
3223            'You logged in' in self.browser.contents)
3224        # Students don't see personal_updated field in edit form
3225        self.browser.open(self.edit_personal_path)
3226        self.assertFalse('Updated' in self.browser.contents)
3227        self.browser.open(self.personal_path)
3228        self.assertTrue('Updated' in self.browser.contents)
3229        self.browser.getLink("Logout").click()
3230        delta = timedelta(days=181)
3231        self.student.personal_updated = datetime.utcnow() - delta
3232        self.browser.open(self.login_path)
3233        self.browser.getControl(name="form.login").value = self.student_id
3234        self.browser.getControl(name="form.password").value = 'spwd'
3235        self.browser.getControl("Login").click()
3236        self.assertEqual(self.browser.url, self.edit_personal_path)
3237        self.assertTrue(
3238            'Your personal data record is outdated.' in self.browser.contents)
3239
3240    def test_request_transcript(self):
3241        IWorkflowState(self.student).setState('graduated')
3242        self.browser.open(self.login_path)
3243        self.browser.getControl(name="form.login").value = self.student_id
3244        self.browser.getControl(name="form.password").value = 'spwd'
3245        self.browser.getControl("Login").click()
3246        self.assertMatches(
3247            '...You logged in...', self.browser.contents)
3248        # Create payment ticket
3249        self.browser.open(self.payments_path)
3250        self.browser.open(self.payments_path + '/addop')
3251        self.browser.getControl(name="form.p_category").value = ['transcript']
3252        self.browser.getControl("Create ticket").click()
3253        ctrl = self.browser.getControl(name='val_id')
3254        value = ctrl.options[0]
3255        self.browser.getLink(value).click()
3256        self.assertMatches('...Amount Authorized...',
3257                           self.browser.contents)
3258        self.assertEqual(self.student['payments'][value].amount_auth, 4567.0)
3259        # Student is the payer of the payment ticket.
3260        payer = IPayer(self.student['payments'][value])
3261        self.assertEqual(payer.display_fullname, 'Anna Tester')
3262        self.assertEqual(payer.id, self.student_id)
3263        self.assertEqual(payer.faculty, 'fac1')
3264        self.assertEqual(payer.department, 'dep1')
3265        # We simulate the approval and fetch the pin
3266        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
3267        self.browser.open(self.browser.url + '/fake_approve')
3268        self.assertMatches('...Payment approved...',
3269                          self.browser.contents)
3270        pin = self.app['accesscodes']['TSC-0'].keys()[0]
3271        parts = pin.split('-')[1:]
3272        tscseries, tscnumber = parts
3273        # Student can use the pin to send the transcript request
3274        self.browser.open(self.student_path)
3275        self.browser.getLink("Request transcript").click()
3276        self.browser.getControl(name="ac_series").value = tscseries
3277        self.browser.getControl(name="ac_number").value = tscnumber
3278        self.browser.getControl(name="comment").value = 'Comment line 1 \nComment line2'
3279        self.browser.getControl(name="address").value = 'Address line 1 \nAddress line2'
3280        self.browser.getControl("Submit").click()
3281        self.assertMatches('...Transcript processing has been started...',
3282                          self.browser.contents)
3283        self.assertEqual(self.student.state, 'transcript requested')
3284        self.assertMatches(
3285            '... UTC K1000000 wrote:\n\nComment line 1 \n'
3286            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
3287            'Address line2\n\n', self.student.transcript_comment)
3288        # The comment has been logged
3289        logfile = os.path.join(
3290            self.app['datacenter'].storage, 'logs', 'students.log')
3291        logcontent = open(logfile).read()
3292        self.assertTrue(
3293            'K1000000 - students.browser.StudentTranscriptRequestPage - '
3294            'K1000000 - comment: Comment line 1 <br>Comment line2\n'
3295            in logcontent)
3296
3297    def test_late_registration(self):
3298        # Login
3299        delta = timedelta(days=10)
3300        self.app['configuration'][
3301            '2004'].coursereg_deadline = datetime.now(pytz.utc) - delta
3302        IWorkflowState(self.student).setState('school fee paid')
3303        self.browser.open(self.login_path)
3304        self.browser.getControl(name="form.login").value = self.student_id
3305        self.browser.getControl(name="form.password").value = 'spwd'
3306        self.browser.getControl("Login").click()
3307        self.browser.open(self.payments_path)
3308        self.browser.open(self.payments_path + '/addop')
3309        self.browser.getControl(name="form.p_category").value = ['late_registration']
3310        self.browser.getControl("Create ticket").click()
3311        self.assertMatches('...ticket created...',
3312                           self.browser.contents)
3313        self.browser.open(self.payments_path)
3314        ctrl = self.browser.getControl(name='val_id')
3315        value = ctrl.options[0]
3316        self.browser.getLink("Study Course").click()
3317        self.browser.getLink("Add course list").click()
3318        self.assertMatches('...Add current level 100 (Year 1)...',
3319                           self.browser.contents)
3320        self.browser.getControl("Create course list now").click()
3321        self.browser.getLink("100").click()
3322        self.browser.getLink("Edit course list").click()
3323        self.browser.getControl("Register course list").click()
3324        self.assertTrue('Course registration has ended. Please pay' in self.browser.contents)
3325        self.student['payments'][value].approve()
3326        self.browser.getControl("Register course list").click()
3327        self.assertTrue('Course list has been registered' in self.browser.contents)
3328        self.assertEqual(self.student.state, 'courses registered')
3329
3330
3331class StudentRequestPWTests(StudentsFullSetup):
3332    # Tests for student registration
3333
3334    layer = FunctionalLayer
3335
3336    def test_request_pw(self):
3337        # Student with wrong number can't be found.
3338        self.browser.open('http://localhost/app/requestpw')
3339        self.browser.getControl(name="form.lastname").value = 'Tester'
3340        self.browser.getControl(name="form.number").value = 'anynumber'
3341        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
3342        self.browser.getControl("Send login credentials").click()
3343        self.assertTrue('No student record found.'
3344            in self.browser.contents)
3345        # Anonymous is not informed that lastname verification failed.
3346        # It seems that the record doesn't exist.
3347        self.browser.open('http://localhost/app/requestpw')
3348        self.browser.getControl(name="form.lastname").value = 'Johnny'
3349        self.browser.getControl(name="form.number").value = '123'
3350        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
3351        self.browser.getControl("Send login credentials").click()
3352        self.assertTrue('No student record found.'
3353            in self.browser.contents)
3354        # Even with the correct lastname we can't register if a
3355        # password has been set and used.
3356        self.browser.getControl(name="form.lastname").value = 'Tester'
3357        self.browser.getControl(name="form.number").value = '123'
3358        self.browser.getControl("Send login credentials").click()
3359        self.assertTrue('Your password has already been set and used.'
3360            in self.browser.contents)
3361        self.browser.open('http://localhost/app/requestpw')
3362        self.app['students'][self.student_id].password = None
3363        # The lastname field, used for verification, is not case-sensitive.
3364        self.browser.getControl(name="form.lastname").value = 'tESTer'
3365        self.browser.getControl(name="form.number").value = '123'
3366        self.browser.getControl(name="form.email").value = 'new@yy.zz'
3367        self.browser.getControl("Send login credentials").click()
3368        # Yeah, we succeded ...
3369        self.assertTrue('Your password request was successful.'
3370            in self.browser.contents)
3371        # We can also use the matric_number instead.
3372        self.browser.open('http://localhost/app/requestpw')
3373        self.browser.getControl(name="form.lastname").value = 'tESTer'
3374        self.browser.getControl(name="form.number").value = '234'
3375        self.browser.getControl(name="form.email").value = 'new@yy.zz'
3376        self.browser.getControl("Send login credentials").click()
3377        self.assertTrue('Your password request was successful.'
3378            in self.browser.contents)
3379        # ... and  student can be found in the catalog via the email address
3380        cat = queryUtility(ICatalog, name='students_catalog')
3381        results = list(
3382            cat.searchResults(
3383            email=('new@yy.zz', 'new@yy.zz')))
3384        self.assertEqual(self.student,results[0])
3385        logfile = os.path.join(
3386            self.app['datacenter'].storage, 'logs', 'main.log')
3387        logcontent = open(logfile).read()
3388        self.assertTrue('zope.anybody - students.browser.StudentRequestPasswordPage - '
3389                        '234 (K1000000) - new@yy.zz' in logcontent)
3390        return
3391
3392    def test_student_locked_level_forms(self):
3393
3394        # Add two study levels, one current and one previous
3395        studylevel = createObject(u'waeup.StudentStudyLevel')
3396        studylevel.level = 100
3397        self.student['studycourse'].addStudentStudyLevel(
3398            self.certificate, studylevel)
3399        studylevel = createObject(u'waeup.StudentStudyLevel')
3400        studylevel.level = 200
3401        self.student['studycourse'].addStudentStudyLevel(
3402            self.certificate, studylevel)
3403        IWorkflowState(self.student).setState('school fee paid')
3404        self.student['studycourse'].current_level = 200
3405
3406        self.browser.open(self.login_path)
3407        self.browser.getControl(name="form.login").value = self.student_id
3408        self.browser.getControl(name="form.password").value = 'spwd'
3409        self.browser.getControl("Login").click()
3410
3411        self.browser.open(self.student_path + '/studycourse/200/edit')
3412        self.assertFalse('The requested form is locked' in self.browser.contents)
3413        self.browser.open(self.student_path + '/studycourse/100/edit')
3414        self.assertTrue('The requested form is locked' in self.browser.contents)
3415
3416        self.browser.open(self.student_path + '/studycourse/200/ctadd')
3417        self.assertFalse('The requested form is locked' in self.browser.contents)
3418        self.browser.open(self.student_path + '/studycourse/100/ctadd')
3419        self.assertTrue('The requested form is locked' in self.browser.contents)
3420
3421        IWorkflowState(self.student).setState('courses registered')
3422        self.browser.open(self.student_path + '/studycourse/200/edit')
3423        self.assertTrue('The requested form is locked' in self.browser.contents)
3424        self.browser.open(self.student_path + '/studycourse/200/ctadd')
3425        self.assertTrue('The requested form is locked' in self.browser.contents)
3426
3427
3428class PublicPagesTests(StudentsFullSetup):
3429    # Tests for simple webservices
3430
3431    layer = FunctionalLayer
3432
3433    def test_paymentrequest(self):
3434        payment = createObject('waeup.StudentOnlinePayment')
3435        payment.p_category = u'schoolfee'
3436        payment.p_session = self.student.current_session
3437        payment.p_item = u'My Certificate'
3438        payment.p_id = u'anyid'
3439        self.student['payments']['anykey'] = payment
3440        # Request information about unpaid payment ticket
3441        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
3442        self.assertEqual(self.browser.contents, '-1')
3443        # Request information about paid payment ticket
3444        payment.p_state = u'paid'
3445        notify(grok.ObjectModifiedEvent(payment))
3446        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
3447        self.assertEqual(self.browser.contents,
3448            'FULL_NAME=Anna Tester&FACULTY=fac1&DEPARTMENT=dep1'
3449            '&PAYMENT_ITEM=My Certificate&PAYMENT_CATEGORY=School Fee'
3450            '&ACADEMIC_SESSION=2004/2005&MATRIC_NUMBER=234&REG_NUMBER=123'
3451            '&FEE_AMOUNT=0.0')
3452        self.browser.open('http://localhost/app/paymentrequest?NONSENSE=nonsense')
3453        self.assertEqual(self.browser.contents, '-1')
3454        self.browser.open('http://localhost/app/paymentrequest?P_ID=nonsense')
3455        self.assertEqual(self.browser.contents, '-1')
3456
3457class StudentDataExportTests(StudentsFullSetup, FunctionalAsyncTestCase):
3458    # Tests for StudentsContainer class views and pages
3459
3460    layer = FunctionalLayer
3461
3462    def wait_for_export_job_completed(self):
3463        # helper function waiting until the current export job is completed
3464        manager = getUtility(IJobManager)
3465        job_id = self.app['datacenter'].running_exports[0][0]
3466        job = manager.get(job_id)
3467        wait_for_result(job)
3468        return job_id
3469
3470    def test_datacenter_export(self):
3471        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3472        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
3473        self.browser.getControl(name="exporter").value = ['bursary']
3474        self.browser.getControl(name="session").value = ['2004']
3475        self.browser.getControl(name="level").value = ['100']
3476        self.browser.getControl(name="mode").value = ['ug_ft']
3477        self.browser.getControl(name="payments_start").value = '13/12/2012'
3478        self.browser.getControl(name="payments_end").value = '14/12/2012'
3479        self.browser.getControl("Create CSV file").click()
3480
3481        # When the job is finished and we reload the page...
3482        job_id = self.wait_for_export_job_completed()
3483        # ... the csv file can be downloaded ...
3484        self.browser.open('http://localhost/app/datacenter/@@export')
3485        self.browser.getLink("Download").click()
3486        self.assertEqual(self.browser.headers['content-type'],
3487            'text/csv; charset=UTF-8')
3488        self.assertTrue(
3489            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
3490            self.browser.headers['content-disposition'])
3491        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3492        job_id = self.app['datacenter'].running_exports[0][0]
3493        # ... and discarded
3494        self.browser.open('http://localhost/app/datacenter/@@export')
3495        self.browser.getControl("Discard").click()
3496        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3497        # Creation, downloading and discarding is logged
3498        logfile = os.path.join(
3499            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3500        logcontent = open(logfile).read()
3501        self.assertTrue(
3502            'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
3503            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
3504            '13/12/2012, 14/12/2012), job_id=%s'
3505            % job_id in logcontent
3506            )
3507        self.assertTrue(
3508            'zope.mgr - browser.pages.ExportCSVView '
3509            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
3510            % (job_id, job_id) in logcontent
3511            )
3512        self.assertTrue(
3513            'zope.mgr - browser.pages.ExportCSVPage '
3514            '- discarded: job_id=%s' % job_id in logcontent
3515            )
3516
3517    def test_datacenter_export_selected(self):
3518        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3519        self.browser.open('http://localhost/app/datacenter/@@exportselected')
3520        self.browser.getControl(name="exporter").value = ['students']
3521        self.browser.getControl(name="students").value = 'K1000000'
3522        self.browser.getControl("Create CSV file").click()
3523        # When the job is finished and we reload the page...
3524        job_id = self.wait_for_export_job_completed()
3525        # ... the csv file can be downloaded ...
3526        self.browser.open('http://localhost/app/datacenter/@@export')
3527        self.browser.getLink("Download").click()
3528        self.assertEqual(self.browser.headers['content-type'],
3529            'text/csv; charset=UTF-8')
3530        self.assertTrue(
3531            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3532            self.browser.headers['content-disposition'])
3533        self.assertTrue(
3534            'adm_code,clr_code,date_of_birth,email,employer,'
3535            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
3536            'officer_comment,perm_address,personal_updated,phone,reg_number,'
3537            'sex,student_id,suspended,suspended_comment,transcript_comment,'
3538            'password,state,history,certcode,is_postgrad,current_level,'
3539            'current_session\r\n'
3540            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,'
3541            '1234#,123,m,K1000000,0,,,{SSHA}' in self.browser.contents)
3542
3543    def test_payment_dates(self):
3544        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3545        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
3546        self.browser.getControl(name="exporter").value = ['bursary']
3547        self.browser.getControl(name="session").value = ['2004']
3548        self.browser.getControl(name="level").value = ['100']
3549        self.browser.getControl(name="mode").value = ['ug_ft']
3550        self.browser.getControl(name="payments_start").value = '13/12/2012'
3551        # If one payment date is missing, an error message appears
3552        self.browser.getControl(name="payments_end").value = ''
3553        self.browser.getControl("Create CSV file").click()
3554        self.assertTrue('Payment dates do not match format d/m/Y'
3555            in self.browser.contents)
3556
3557    def test_faculties_export(self):
3558        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3559        facs_path = 'http://localhost/app/faculties'
3560        self.browser.open(facs_path)
3561        self.browser.getLink("Export student data").click()
3562        self.browser.getControl("Configure new export").click()
3563        self.browser.getControl(name="exporter").value = ['bursary']
3564        self.browser.getControl(name="session").value = ['2004']
3565        self.browser.getControl(name="level").value = ['100']
3566        self.browser.getControl(name="mode").value = ['ug_ft']
3567        self.browser.getControl(name="payments_start").value = '13/12/2012'
3568        self.browser.getControl(name="payments_end").value = '14/12/2012'
3569        self.browser.getControl("Create CSV file").click()
3570
3571        # When the job is finished and we reload the page...
3572        job_id = self.wait_for_export_job_completed()
3573        self.browser.open(facs_path + '/exports')
3574        # ... the csv file can be downloaded ...
3575        self.browser.getLink("Download").click()
3576        self.assertEqual(self.browser.headers['content-type'],
3577            'text/csv; charset=UTF-8')
3578        self.assertTrue(
3579            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
3580            self.browser.headers['content-disposition'])
3581        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3582        job_id = self.app['datacenter'].running_exports[0][0]
3583        # ... and discarded
3584        self.browser.open(facs_path + '/exports')
3585        self.browser.getControl("Discard").click()
3586        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3587        # Creation, downloading and discarding is logged
3588        logfile = os.path.join(
3589            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3590        logcontent = open(logfile).read()
3591        self.assertTrue(
3592            'zope.mgr - students.browser.FacultiesExportJobContainerJobConfig '
3593            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
3594            '13/12/2012, 14/12/2012), job_id=%s'
3595            % job_id in logcontent
3596            )
3597        self.assertTrue(
3598            'zope.mgr - students.browser.ExportJobContainerDownload '
3599            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
3600            % (job_id, job_id) in logcontent
3601            )
3602        self.assertTrue(
3603            'zope.mgr - students.browser.ExportJobContainerOverview '
3604            '- discarded: job_id=%s' % job_id in logcontent
3605            )
3606
3607    def test_faculty_export(self):
3608        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3609        fac1_path = 'http://localhost/app/faculties/fac1'
3610        self.browser.open(fac1_path)
3611        self.browser.getLink("Export student data").click()
3612        self.browser.getControl("Configure new export").click()
3613        self.browser.getControl(name="exporter").value = ['students']
3614        self.browser.getControl(name="session").value = ['2004']
3615        self.browser.getControl(name="level").value = ['100']
3616        self.browser.getControl(name="mode").value = ['ug_ft']
3617        # The testbrowser does not hide the payment period fields, but
3618        # values are ignored when using the students exporter.
3619        self.browser.getControl(name="payments_start").value = '13/12/2012'
3620        self.browser.getControl(name="payments_end").value = '14/12/2012'
3621        self.browser.getControl("Create CSV file").click()
3622
3623        # When the job is finished and we reload the page...
3624        job_id = self.wait_for_export_job_completed()
3625        self.browser.open(fac1_path + '/exports')
3626        # ... the csv file can be downloaded ...
3627        self.browser.getLink("Download").click()
3628        self.assertEqual(self.browser.headers['content-type'],
3629            'text/csv; charset=UTF-8')
3630        self.assertTrue(
3631            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3632            self.browser.headers['content-disposition'])
3633        self.assertTrue(
3634            'adm_code,clr_code,date_of_birth,email,employer,'
3635            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
3636            'officer_comment,perm_address,personal_updated,phone,reg_number,'
3637            'sex,student_id,suspended,suspended_comment,transcript_comment,'
3638            'password,state,history,certcode,is_postgrad,current_level,'
3639            'current_session\r\n'
3640            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,'
3641            '1234#,123,m,K1000000,0,,,{SSHA}' in self.browser.contents)
3642        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3643        job_id = self.app['datacenter'].running_exports[0][0]
3644        # ... and discarded
3645        self.browser.open(fac1_path + '/exports')
3646        self.browser.getControl("Discard").click()
3647        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3648        # Creation, downloading and discarding is logged
3649        logfile = os.path.join(
3650            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3651        logcontent = open(logfile).read()
3652        self.assertTrue(
3653            'zope.mgr - students.browser.FacultyExportJobContainerJobConfig '
3654            '- exported: students (2004, 100, ug_ft, fac1, None, None, '
3655            '13/12/2012, 14/12/2012), job_id=%s'
3656            % job_id in logcontent
3657            )
3658        self.assertTrue(
3659            'zope.mgr - students.browser.ExportJobContainerDownload '
3660            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3661            % (job_id, job_id) in logcontent
3662            )
3663        self.assertTrue(
3664            'zope.mgr - students.browser.ExportJobContainerOverview '
3665            '- discarded: job_id=%s' % job_id in logcontent
3666            )
3667
3668    def test_department_export(self):
3669        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3670        dep1_path = 'http://localhost/app/faculties/fac1/dep1'
3671        self.browser.open(dep1_path)
3672        self.browser.getLink("Export student data").click()
3673        self.browser.getControl("Configure new export").click()
3674        self.browser.getControl(name="exporter").value = ['students']
3675        self.browser.getControl(name="session").value = ['2004']
3676        self.browser.getControl(name="level").value = ['100']
3677        self.browser.getControl(name="mode").value = ['ug_ft']
3678        # The testbrowser does not hide the payment period fields, but
3679        # values are ignored when using the students exporter.
3680        self.browser.getControl(name="payments_start").value = '13/12/2012'
3681        self.browser.getControl(name="payments_end").value = '14/12/2012'
3682        self.browser.getControl("Create CSV file").click()
3683
3684        # When the job is finished and we reload the page...
3685        job_id = self.wait_for_export_job_completed()
3686        self.browser.open(dep1_path + '/exports')
3687        # ... the csv file can be downloaded ...
3688        self.browser.getLink("Download").click()
3689        self.assertEqual(self.browser.headers['content-type'],
3690            'text/csv; charset=UTF-8')
3691        self.assertTrue(
3692            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3693            self.browser.headers['content-disposition'])
3694        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3695        job_id = self.app['datacenter'].running_exports[0][0]
3696        # ... and discarded
3697        self.browser.open(dep1_path + '/exports')
3698        self.browser.getControl("Discard").click()
3699        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3700        # Creation, downloading and discarding is logged
3701        logfile = os.path.join(
3702            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3703        logcontent = open(logfile).read()
3704        self.assertTrue(
3705            'zope.mgr - students.browser.DepartmentExportJobContainerJobConfig '
3706            '- exported: students (2004, 100, ug_ft, None, dep1, None, '
3707            '13/12/2012, 14/12/2012), job_id=%s'
3708            % job_id in logcontent
3709            )
3710        self.assertTrue(
3711            'zope.mgr - students.browser.ExportJobContainerDownload '
3712            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3713            % (job_id, job_id) in logcontent
3714            )
3715        self.assertTrue(
3716            'zope.mgr - students.browser.ExportJobContainerOverview '
3717            '- discarded: job_id=%s' % job_id in logcontent
3718            )
3719
3720    def test_certificate_export(self):
3721        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3722        cert1_path = 'http://localhost/app/faculties/fac1/dep1/certificates/CERT1'
3723        self.browser.open(cert1_path)
3724        self.browser.getLink("Export student data").click()
3725        self.browser.getControl("Configure new export").click()
3726        self.browser.getControl(name="exporter").value = ['students']
3727        self.browser.getControl(name="session").value = ['2004']
3728        self.browser.getControl(name="level").value = ['100']
3729        self.browser.getControl("Create CSV file").click()
3730
3731        # When the job is finished and we reload the page...
3732        job_id = self.wait_for_export_job_completed()
3733        self.browser.open(cert1_path + '/exports')
3734        # ... the csv file can be downloaded ...
3735        self.browser.getLink("Download").click()
3736        self.assertEqual(self.browser.headers['content-type'],
3737            'text/csv; charset=UTF-8')
3738        self.assertTrue(
3739            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3740            self.browser.headers['content-disposition'])
3741        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3742        job_id = self.app['datacenter'].running_exports[0][0]
3743        # ... and discarded
3744        self.browser.open(cert1_path + '/exports')
3745        self.browser.getControl("Discard").click()
3746        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3747        # Creation, downloading and discarding is logged
3748        logfile = os.path.join(
3749            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3750        logcontent = open(logfile).read()
3751        self.assertTrue(
3752            'zope.mgr - students.browser.CertificateExportJobContainerJobConfig '
3753            '- exported: students (2004, 100, None, None, None, CERT1, None, None), '
3754            'job_id=%s'
3755            % job_id in logcontent
3756            )
3757        self.assertTrue(
3758            'zope.mgr - students.browser.ExportJobContainerDownload '
3759            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3760            % (job_id, job_id) in logcontent
3761            )
3762        self.assertTrue(
3763            'zope.mgr - students.browser.ExportJobContainerOverview '
3764            '- discarded: job_id=%s' % job_id in logcontent
3765            )
3766
3767    def test_course_export_students(self):
3768        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3769        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
3770        self.browser.open(course1_path)
3771        self.browser.getLink("Export student data").click()
3772        self.browser.getControl("Configure new export").click()
3773        self.browser.getControl(name="exporter").value = ['students']
3774        self.browser.getControl(name="session").value = ['2004']
3775        self.browser.getControl(name="level").value = ['100']
3776        self.browser.getControl("Create CSV file").click()
3777
3778        # When the job is finished and we reload the page...
3779        job_id = self.wait_for_export_job_completed()
3780        self.browser.open(course1_path + '/exports')
3781        # ... the csv file can be downloaded ...
3782        self.browser.getLink("Download").click()
3783        self.assertEqual(self.browser.headers['content-type'],
3784            'text/csv; charset=UTF-8')
3785        self.assertTrue(
3786            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3787            self.browser.headers['content-disposition'])
3788        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3789        job_id = self.app['datacenter'].running_exports[0][0]
3790        # ... and discarded
3791        self.browser.open(course1_path + '/exports')
3792        self.browser.getControl("Discard").click()
3793        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3794        # Creation, downloading and discarding is logged
3795        logfile = os.path.join(
3796            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3797        logcontent = open(logfile).read()
3798        self.assertTrue(
3799            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
3800            '- exported: students (2004, 100, COURSE1), job_id=%s'
3801            % job_id in logcontent
3802            )
3803        self.assertTrue(
3804            'zope.mgr - students.browser.ExportJobContainerDownload '
3805            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3806            % (job_id, job_id) in logcontent
3807            )
3808        self.assertTrue(
3809            'zope.mgr - students.browser.ExportJobContainerOverview '
3810            '- discarded: job_id=%s' % job_id in logcontent
3811            )
3812
3813    def test_course_export_coursetickets(self):
3814        # We add study level 100 to the student's studycourse
3815        studylevel = StudentStudyLevel()
3816        studylevel.level = 100
3817        studylevel.level_session = 2004
3818        self.student['studycourse'].addStudentStudyLevel(
3819            self.certificate,studylevel)
3820        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3821        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
3822        self.browser.open(course1_path)
3823        self.browser.getLink("Export student data").click()
3824        self.browser.getControl("Configure new export").click()
3825        self.browser.getControl(name="exporter").value = ['coursetickets']
3826        self.browser.getControl(name="session").value = ['2004']
3827        self.browser.getControl(name="level").value = ['100']
3828        self.browser.getControl("Create CSV file").click()
3829        # When the job is finished and we reload the page...
3830        job_id = self.wait_for_export_job_completed()
3831        self.browser.open(course1_path + '/exports')
3832        # ... the csv file can be downloaded ...
3833        self.browser.getLink("Download").click()
3834        self.assertEqual(self.browser.headers['content-type'],
3835            'text/csv; charset=UTF-8')
3836        self.assertTrue(
3837            'filename="WAeUP.Kofa_coursetickets_%s.csv' % job_id in
3838            self.browser.headers['content-disposition'])
3839        # ... and contains the course ticket COURSE1
3840        self.assertEqual(self.browser.contents,
3841            'automatic,carry_over,code,credits,dcode,fcode,level,'
3842            'level_session,mandatory,passmark,score,semester,title,'
3843            'student_id,certcode,display_fullname\r\n1,0,COURSE1,10,'
3844            'dep1,fac1,100,2004,1,40,,1,'
3845            'Unnamed Course,K1000000,CERT1,Anna Tester\r\n')
3846
3847        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3848        job_id = self.app['datacenter'].running_exports[0][0]
3849        # Thew job can be discarded
3850        self.browser.open(course1_path + '/exports')
3851        self.browser.getControl("Discard").click()
3852        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3853        # Creation, downloading and discarding is logged
3854        logfile = os.path.join(
3855            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3856        logcontent = open(logfile).read()
3857        self.assertTrue(
3858            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
3859            '- exported: coursetickets (2004, 100, COURSE1), job_id=%s'
3860            % job_id in logcontent
3861            )
3862        self.assertTrue(
3863            'zope.mgr - students.browser.ExportJobContainerDownload '
3864            '- downloaded: WAeUP.Kofa_coursetickets_%s.csv, job_id=%s'
3865            % (job_id, job_id) in logcontent
3866            )
3867        self.assertTrue(
3868            'zope.mgr - students.browser.ExportJobContainerOverview '
3869            '- discarded: job_id=%s' % job_id in logcontent
3870            )
3871
3872    def test_export_departmet_officers(self):
3873        # Create department officer
3874        self.app['users'].addUser('mrdepartment', 'mrdepartmentsecret')
3875        self.app['users']['mrdepartment'].email = 'mrdepartment@foo.ng'
3876        self.app['users']['mrdepartment'].title = 'Carlo Pitter'
3877        # Assign local role
3878        department = self.app['faculties']['fac1']['dep1']
3879        prmlocal = IPrincipalRoleManager(department)
3880        prmlocal.assignRoleToPrincipal('waeup.local.DepartmentOfficer', 'mrdepartment')
3881        # Login as department officer
3882        self.browser.open(self.login_path)
3883        self.browser.getControl(name="form.login").value = 'mrdepartment'
3884        self.browser.getControl(name="form.password").value = 'mrdepartmentsecret'
3885        self.browser.getControl("Login").click()
3886        self.assertMatches('...You logged in...', self.browser.contents)
3887        self.browser.open("http://localhost/app/faculties/fac1/dep1")
3888        self.browser.getLink("Export student data").click()
3889        self.browser.getControl("Configure new export").click()
3890        # Only the paymentsoverview exporter is available for department officers
3891        self.assertFalse('<option value="students">' in self.browser.contents)
3892        self.assertTrue(
3893            '<option value="paymentsoverview">' in self.browser.contents)
3894        self.browser.getControl(name="exporter").value = ['paymentsoverview']
3895        self.browser.getControl(name="session").value = ['2004']
3896        self.browser.getControl(name="level").value = ['100']
3897        self.browser.getControl("Create CSV file").click()
3898        self.assertTrue('Export started' in self.browser.contents)
3899        # Thew job can be discarded
3900        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3901        #job_id = self.app['datacenter'].running_exports[0][0]
3902        job_id = self.wait_for_export_job_completed()
3903        self.browser.open("http://localhost/app/faculties/fac1/dep1/exports")
3904        self.browser.getControl("Discard").click()
3905        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3906
3907    def test_export_bursary_officers(self):
3908        # Create bursary officer
3909        self.app['users'].addUser('mrbursary', 'mrbursarysecret')
3910        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
3911        self.app['users']['mrbursary'].title = 'Carlo Pitter'
3912        prmglobal = IPrincipalRoleManager(self.app)
3913        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
3914        # Login as bursary officer
3915        self.browser.open(self.login_path)
3916        self.browser.getControl(name="form.login").value = 'mrbursary'
3917        self.browser.getControl(name="form.password").value = 'mrbursarysecret'
3918        self.browser.getControl("Login").click()
3919        self.assertMatches('...You logged in...', self.browser.contents)
3920        self.browser.getLink("Academics").click()
3921        self.browser.getLink("Export student data").click()
3922        self.browser.getControl("Configure new export").click()
3923        # Only the bursary exporter is available for bursary officers
3924        # not only at facultiescontainer level ...
3925        self.assertFalse('<option value="students">' in self.browser.contents)
3926        self.assertTrue('<option value="bursary">' in self.browser.contents)
3927        self.browser.getControl(name="exporter").value = ['bursary']
3928        self.browser.getControl(name="session").value = ['2004']
3929        self.browser.getControl(name="level").value = ['100']
3930        self.browser.getControl("Create CSV file").click()
3931        self.assertTrue('Export started' in self.browser.contents)
3932        # ... but also at other levels
3933        self.browser.open('http://localhost/app/faculties/fac1/dep1')
3934        self.browser.getLink("Export student data").click()
3935        self.browser.getControl("Configure new export").click()
3936        self.assertFalse('<option value="students">' in self.browser.contents)
3937        self.assertTrue('<option value="bursary">' in self.browser.contents)
3938        # Thew job can be discarded
3939        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3940        #job_id = self.app['datacenter'].running_exports[0][0]
3941        job_id = self.wait_for_export_job_completed()
3942        self.browser.open('http://localhost/app/faculties/exports')
3943        self.browser.getControl("Discard").click()
3944        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
Note: See TracBrowser for help on using the repository browser.