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

Last change on this file since 15505 was 15423, checked in by Henrik Bettermann, 5 years ago

Add attendance_sheet.pdf view.

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