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

Last change on this file since 14086 was 13936, checked in by Henrik Bettermann, 8 years ago

Divide editscorespage.pt into two parts, one for file upload and one for table form.

Add Help modal.

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